@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.
- package/dist/index.cjs +161 -144
- package/dist/index.d.cts +96 -104
- package/dist/index.d.ts +96 -104
- package/dist/index.js +161 -144
- package/package.json +1 -1
- package/src/application-requirements.ts +12 -6
- package/src/applications.generated.ts +61 -114
- package/src/client.test.ts +74 -0
- package/src/client.ts +101 -0
- package/src/index.test.ts +69 -0
- package/src/index.ts +1 -0
- package/src/modules/crm.test.ts +208 -0
- package/src/modules/identity.test.ts +165 -0
- package/src/modules/p2p.test.ts +286 -0
- package/src/oauth.test.ts +103 -0
- package/src/services.generated.ts +14 -15
- package/src/services.test.ts +19 -8
- package/src/widgets.test.ts +255 -1
- package/src/widgets.ts +5 -3
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type ApplicationId,
|
|
5
5
|
} from './applications.generated.js';
|
|
6
6
|
|
|
7
|
-
export const APPLICATION_REQUIREMENTS_VERSION = '2026-
|
|
7
|
+
export const APPLICATION_REQUIREMENTS_VERSION = '2026-06-07.1';
|
|
8
8
|
|
|
9
9
|
export type ApplicationRequirementScope =
|
|
10
10
|
| 'all'
|
|
@@ -125,16 +125,20 @@ export const APPLICATION_REQUIREMENTS = [
|
|
|
125
125
|
'Browser applications should feel like one platform and should not create isolated navigation or account surfaces.',
|
|
126
126
|
appliesTo: ['browser'],
|
|
127
127
|
evidence: [
|
|
128
|
-
'@topolo/
|
|
129
|
-
'
|
|
130
|
-
'Account menu and sign-out behavior use TopoloAppShell or TopoloAccountMenu',
|
|
128
|
+
'@topolo-io/app-shell is used for the platform shell where the app has authenticated UI',
|
|
129
|
+
'@topolo-io/app-launcher keeps the shared launcher/app switcher reachable from authenticated app chrome',
|
|
130
|
+
'Account menu and sign-out behavior use TopoloAppShell or the ui-kit TopoloAccountMenu primitive',
|
|
131
|
+
'Repeated app body surfaces use shared TopoloAppLayout primitives instead of local stat cards, empty states, or status badges',
|
|
132
|
+
'Local preview supports deterministic visual QA fixtures for overview, list, detail, settings, and empty/loading states',
|
|
131
133
|
'Authenticated app body content stays inside the viewport on mobile and desktop widths',
|
|
132
134
|
],
|
|
133
135
|
implementation: [
|
|
134
|
-
'Use TopoloAppShell for authenticated app layouts',
|
|
135
|
-
'Use TopoloAccountMenu when a partial-shell surface still needs account chrome',
|
|
136
|
+
'Use @topolo-io/app-shell TopoloAppShell for authenticated app layouts',
|
|
137
|
+
'Use the @topolo-io/ui-kit TopoloAccountMenu primitive when a partial-shell surface still needs account chrome',
|
|
136
138
|
'Pass app-specific account actions as additive menu items instead of forking dropdowns',
|
|
137
139
|
'Use shared design tokens and platform UI primitives before creating local equivalents',
|
|
140
|
+
'Use TopoloAppMetricCard, TopoloAppEmptyState, TopoloAppStatusBadge, TopoloAppGrid, and TopoloAppSection for repeated app body patterns',
|
|
141
|
+
'Keep visual QA fixtures available through visualQa=1 or __devpreview=1 so screenshot checks do not depend on live API state',
|
|
138
142
|
'Keep mobile and desktop shell behavior aligned with the platform UI kit',
|
|
139
143
|
'Constrain app-owned workspace wrappers with min-width: 0, width: auto, and viewport-aware max-width so shell padding cannot create page-level horizontal overflow',
|
|
140
144
|
],
|
|
@@ -218,11 +222,13 @@ export const APPLICATION_REQUIREMENTS = [
|
|
|
218
222
|
evidence: [
|
|
219
223
|
'The app-owned API exposes GET /api/widget',
|
|
220
224
|
'The endpoint returns TopoloWidgetApiResponse from @topolo/sdk',
|
|
225
|
+
'Every returned widget includes snapshot.summary with app-owned operational prose',
|
|
221
226
|
'The endpoint derives organization/user context from the app auth boundary',
|
|
222
227
|
],
|
|
223
228
|
implementation: [
|
|
224
229
|
'Import createTopoloWidgetResponse from @topolo/sdk in the app backend',
|
|
225
230
|
'Return native app metrics or action widgets instead of generic launch-card fallbacks',
|
|
231
|
+
'Populate snapshot.summary in the producing app for every widget instead of synthesizing summaries in TopoloOne',
|
|
226
232
|
'Add a route-level test that validates the response with validateTopoloWidgetResponse',
|
|
227
233
|
],
|
|
228
234
|
},
|
|
@@ -49,17 +49,15 @@ export const APPLICATIONS = {
|
|
|
49
49
|
"agent.topolo.app"
|
|
50
50
|
],
|
|
51
51
|
"routes": [],
|
|
52
|
-
"serviceId":
|
|
52
|
+
"serviceId": "agent"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"name": "topolo-agent-api",
|
|
56
56
|
"kind": "worker",
|
|
57
|
-
"apiUrl":
|
|
58
|
-
"domains": [
|
|
59
|
-
"agent-api.topolo.app"
|
|
60
|
-
],
|
|
57
|
+
"apiUrl": null,
|
|
58
|
+
"domains": [],
|
|
61
59
|
"routes": [],
|
|
62
|
-
"serviceId":
|
|
60
|
+
"serviceId": null
|
|
63
61
|
}
|
|
64
62
|
]
|
|
65
63
|
},
|
|
@@ -179,17 +177,15 @@ export const APPLICATIONS = {
|
|
|
179
177
|
"bytes.topolo.app"
|
|
180
178
|
],
|
|
181
179
|
"routes": [],
|
|
182
|
-
"serviceId":
|
|
180
|
+
"serviceId": "bytes"
|
|
183
181
|
},
|
|
184
182
|
{
|
|
185
183
|
"name": "topolo-bytes-api",
|
|
186
184
|
"kind": "worker",
|
|
187
|
-
"apiUrl":
|
|
188
|
-
"domains": [
|
|
189
|
-
"bytes-api.topolo.app"
|
|
190
|
-
],
|
|
185
|
+
"apiUrl": null,
|
|
186
|
+
"domains": [],
|
|
191
187
|
"routes": [],
|
|
192
|
-
"serviceId":
|
|
188
|
+
"serviceId": null
|
|
193
189
|
}
|
|
194
190
|
]
|
|
195
191
|
},
|
|
@@ -288,12 +284,10 @@ export const APPLICATIONS = {
|
|
|
288
284
|
{
|
|
289
285
|
"name": "topolo-commerce-api",
|
|
290
286
|
"kind": "worker",
|
|
291
|
-
"apiUrl":
|
|
292
|
-
"domains": [
|
|
293
|
-
"commerce-api.topolo.app"
|
|
294
|
-
],
|
|
287
|
+
"apiUrl": null,
|
|
288
|
+
"domains": [],
|
|
295
289
|
"routes": [],
|
|
296
|
-
"serviceId":
|
|
290
|
+
"serviceId": null
|
|
297
291
|
},
|
|
298
292
|
{
|
|
299
293
|
"name": "topolo-commerce-guest-web",
|
|
@@ -313,7 +307,7 @@ export const APPLICATIONS = {
|
|
|
313
307
|
"commerce.topolo.app"
|
|
314
308
|
],
|
|
315
309
|
"routes": [],
|
|
316
|
-
"serviceId":
|
|
310
|
+
"serviceId": "commerce"
|
|
317
311
|
},
|
|
318
312
|
{
|
|
319
313
|
"name": "topolo-commerce-station-web",
|
|
@@ -400,12 +394,10 @@ export const APPLICATIONS = {
|
|
|
400
394
|
{
|
|
401
395
|
"name": "topolo-crm-api",
|
|
402
396
|
"kind": "worker",
|
|
403
|
-
"apiUrl":
|
|
404
|
-
"domains": [
|
|
405
|
-
"crm-api.topolo.app"
|
|
406
|
-
],
|
|
397
|
+
"apiUrl": null,
|
|
398
|
+
"domains": [],
|
|
407
399
|
"routes": [],
|
|
408
|
-
"serviceId":
|
|
400
|
+
"serviceId": null
|
|
409
401
|
},
|
|
410
402
|
{
|
|
411
403
|
"name": "topolo-crm-app",
|
|
@@ -415,7 +407,7 @@ export const APPLICATIONS = {
|
|
|
415
407
|
"crm.topolo.app"
|
|
416
408
|
],
|
|
417
409
|
"routes": [],
|
|
418
|
-
"serviceId":
|
|
410
|
+
"serviceId": "crm"
|
|
419
411
|
}
|
|
420
412
|
]
|
|
421
413
|
},
|
|
@@ -538,12 +530,10 @@ export const APPLICATIONS = {
|
|
|
538
530
|
{
|
|
539
531
|
"name": "topolo-feed-api",
|
|
540
532
|
"kind": "worker",
|
|
541
|
-
"apiUrl":
|
|
542
|
-
"domains": [
|
|
543
|
-
"feed-api.topolo.app"
|
|
544
|
-
],
|
|
533
|
+
"apiUrl": null,
|
|
534
|
+
"domains": [],
|
|
545
535
|
"routes": [],
|
|
546
|
-
"serviceId":
|
|
536
|
+
"serviceId": null
|
|
547
537
|
},
|
|
548
538
|
{
|
|
549
539
|
"name": "topolo-feed-web",
|
|
@@ -553,7 +543,7 @@ export const APPLICATIONS = {
|
|
|
553
543
|
"feed.topolo.app"
|
|
554
544
|
],
|
|
555
545
|
"routes": [],
|
|
556
|
-
"serviceId":
|
|
546
|
+
"serviceId": "feed"
|
|
557
547
|
}
|
|
558
548
|
]
|
|
559
549
|
},
|
|
@@ -605,17 +595,15 @@ export const APPLICATIONS = {
|
|
|
605
595
|
"forecast.topolo.app"
|
|
606
596
|
],
|
|
607
597
|
"routes": [],
|
|
608
|
-
"serviceId":
|
|
598
|
+
"serviceId": "forecast"
|
|
609
599
|
},
|
|
610
600
|
{
|
|
611
601
|
"name": "topolo-forecast-api",
|
|
612
602
|
"kind": "worker",
|
|
613
|
-
"apiUrl":
|
|
614
|
-
"domains": [
|
|
615
|
-
"forecast-api.topolo.app"
|
|
616
|
-
],
|
|
603
|
+
"apiUrl": null,
|
|
604
|
+
"domains": [],
|
|
617
605
|
"routes": [],
|
|
618
|
-
"serviceId":
|
|
606
|
+
"serviceId": null
|
|
619
607
|
}
|
|
620
608
|
]
|
|
621
609
|
},
|
|
@@ -757,17 +745,15 @@ export const APPLICATIONS = {
|
|
|
757
745
|
"learn.topolo.app"
|
|
758
746
|
],
|
|
759
747
|
"routes": [],
|
|
760
|
-
"serviceId":
|
|
748
|
+
"serviceId": "learn"
|
|
761
749
|
},
|
|
762
750
|
{
|
|
763
751
|
"name": "topolo-learn-api",
|
|
764
752
|
"kind": "worker",
|
|
765
|
-
"apiUrl":
|
|
766
|
-
"domains": [
|
|
767
|
-
"learn-api.topolo.app"
|
|
768
|
-
],
|
|
753
|
+
"apiUrl": null,
|
|
754
|
+
"domains": [],
|
|
769
755
|
"routes": [],
|
|
770
|
-
"serviceId":
|
|
756
|
+
"serviceId": null
|
|
771
757
|
}
|
|
772
758
|
]
|
|
773
759
|
},
|
|
@@ -813,17 +799,15 @@ export const APPLICATIONS = {
|
|
|
813
799
|
"mail.topolo.app"
|
|
814
800
|
],
|
|
815
801
|
"routes": [],
|
|
816
|
-
"serviceId":
|
|
802
|
+
"serviceId": "mail"
|
|
817
803
|
},
|
|
818
804
|
{
|
|
819
805
|
"name": "topolo-mail-api",
|
|
820
806
|
"kind": "worker",
|
|
821
|
-
"apiUrl":
|
|
822
|
-
"domains": [
|
|
823
|
-
"mail-api.topolo.app"
|
|
824
|
-
],
|
|
807
|
+
"apiUrl": null,
|
|
808
|
+
"domains": [],
|
|
825
809
|
"routes": [],
|
|
826
|
-
"serviceId":
|
|
810
|
+
"serviceId": null
|
|
827
811
|
}
|
|
828
812
|
]
|
|
829
813
|
},
|
|
@@ -840,12 +824,10 @@ export const APPLICATIONS = {
|
|
|
840
824
|
{
|
|
841
825
|
"name": "topolo-mdm-api",
|
|
842
826
|
"kind": "worker",
|
|
843
|
-
"apiUrl":
|
|
844
|
-
"domains": [
|
|
845
|
-
"mdm-api.topolo.app"
|
|
846
|
-
],
|
|
827
|
+
"apiUrl": null,
|
|
828
|
+
"domains": [],
|
|
847
829
|
"routes": [],
|
|
848
|
-
"serviceId":
|
|
830
|
+
"serviceId": null
|
|
849
831
|
},
|
|
850
832
|
{
|
|
851
833
|
"name": "topolo-mdm-console",
|
|
@@ -855,7 +837,7 @@ export const APPLICATIONS = {
|
|
|
855
837
|
"mdm.topolo.app"
|
|
856
838
|
],
|
|
857
839
|
"routes": [],
|
|
858
|
-
"serviceId":
|
|
840
|
+
"serviceId": "mdm"
|
|
859
841
|
}
|
|
860
842
|
]
|
|
861
843
|
},
|
|
@@ -988,20 +970,23 @@ export const APPLICATIONS = {
|
|
|
988
970
|
{
|
|
989
971
|
"name": "topolo-one",
|
|
990
972
|
"kind": "worker",
|
|
991
|
-
"apiUrl":
|
|
992
|
-
"domains": [
|
|
993
|
-
|
|
973
|
+
"apiUrl": "https://one.topolo.app",
|
|
974
|
+
"domains": [
|
|
975
|
+
"one.topolo.app"
|
|
976
|
+
],
|
|
977
|
+
"routes": [
|
|
978
|
+
"one.topolo.app/*"
|
|
979
|
+
],
|
|
994
980
|
"serviceId": null
|
|
995
981
|
},
|
|
996
982
|
{
|
|
997
983
|
"name": "topolo-one-api",
|
|
998
984
|
"kind": "worker",
|
|
999
|
-
"apiUrl": "https://
|
|
985
|
+
"apiUrl": "https://www.topolo.app",
|
|
1000
986
|
"domains": [
|
|
1001
|
-
"
|
|
987
|
+
"www.topolo.app"
|
|
1002
988
|
],
|
|
1003
989
|
"routes": [
|
|
1004
|
-
"one.topolo.app/*",
|
|
1005
990
|
"www.topolo.app/*"
|
|
1006
991
|
],
|
|
1007
992
|
"serviceId": "one"
|
|
@@ -1111,36 +1096,6 @@ export const APPLICATIONS = {
|
|
|
1111
1096
|
}
|
|
1112
1097
|
]
|
|
1113
1098
|
},
|
|
1114
|
-
"pulse": {
|
|
1115
|
-
"id": "pulse",
|
|
1116
|
-
"appDir": "TopoloPulse",
|
|
1117
|
-
"name": "topolo-pulse",
|
|
1118
|
-
"packageName": "topolo-pulse",
|
|
1119
|
-
"productionUrl": "https://pulse.topolo.app",
|
|
1120
|
-
"services": [
|
|
1121
|
-
"pulse"
|
|
1122
|
-
],
|
|
1123
|
-
"deployTargets": [
|
|
1124
|
-
{
|
|
1125
|
-
"name": "topolo-pulse",
|
|
1126
|
-
"kind": "worker",
|
|
1127
|
-
"apiUrl": "https://pulse.topolo.app",
|
|
1128
|
-
"domains": [
|
|
1129
|
-
"pulse.topolo.app"
|
|
1130
|
-
],
|
|
1131
|
-
"routes": [],
|
|
1132
|
-
"serviceId": "pulse"
|
|
1133
|
-
},
|
|
1134
|
-
{
|
|
1135
|
-
"name": "topolo-pulse-api",
|
|
1136
|
-
"kind": "worker",
|
|
1137
|
-
"apiUrl": null,
|
|
1138
|
-
"domains": [],
|
|
1139
|
-
"routes": [],
|
|
1140
|
-
"serviceId": null
|
|
1141
|
-
}
|
|
1142
|
-
]
|
|
1143
|
-
},
|
|
1144
1099
|
"quro": {
|
|
1145
1100
|
"id": "quro",
|
|
1146
1101
|
"appDir": "TopoloQuro",
|
|
@@ -1206,17 +1161,15 @@ export const APPLICATIONS = {
|
|
|
1206
1161
|
"roadmapper.topolo.app"
|
|
1207
1162
|
],
|
|
1208
1163
|
"routes": [],
|
|
1209
|
-
"serviceId":
|
|
1164
|
+
"serviceId": "roadmapper"
|
|
1210
1165
|
},
|
|
1211
1166
|
{
|
|
1212
1167
|
"name": "topolo-roadmapper-api",
|
|
1213
1168
|
"kind": "worker",
|
|
1214
|
-
"apiUrl":
|
|
1215
|
-
"domains": [
|
|
1216
|
-
"roadmapper-api.topolo.app"
|
|
1217
|
-
],
|
|
1169
|
+
"apiUrl": null,
|
|
1170
|
+
"domains": [],
|
|
1218
1171
|
"routes": [],
|
|
1219
|
-
"serviceId":
|
|
1172
|
+
"serviceId": null
|
|
1220
1173
|
}
|
|
1221
1174
|
]
|
|
1222
1175
|
},
|
|
@@ -1268,17 +1221,15 @@ export const APPLICATIONS = {
|
|
|
1268
1221
|
"studio.topolo.app"
|
|
1269
1222
|
],
|
|
1270
1223
|
"routes": [],
|
|
1271
|
-
"serviceId":
|
|
1224
|
+
"serviceId": "social_studio"
|
|
1272
1225
|
},
|
|
1273
1226
|
{
|
|
1274
1227
|
"name": "topolo-social-studio-api",
|
|
1275
1228
|
"kind": "worker",
|
|
1276
|
-
"apiUrl":
|
|
1277
|
-
"domains": [
|
|
1278
|
-
"studio-api.topolo.app"
|
|
1279
|
-
],
|
|
1229
|
+
"apiUrl": null,
|
|
1230
|
+
"domains": [],
|
|
1280
1231
|
"routes": [],
|
|
1281
|
-
"serviceId":
|
|
1232
|
+
"serviceId": null
|
|
1282
1233
|
},
|
|
1283
1234
|
{
|
|
1284
1235
|
"name": "topolo-social-studio-queue",
|
|
@@ -1308,17 +1259,15 @@ export const APPLICATIONS = {
|
|
|
1308
1259
|
"socialize.topolo.app"
|
|
1309
1260
|
],
|
|
1310
1261
|
"routes": [],
|
|
1311
|
-
"serviceId":
|
|
1262
|
+
"serviceId": "socialize"
|
|
1312
1263
|
},
|
|
1313
1264
|
{
|
|
1314
1265
|
"name": "topolo-socialize-api",
|
|
1315
1266
|
"kind": "worker",
|
|
1316
|
-
"apiUrl":
|
|
1317
|
-
"domains": [
|
|
1318
|
-
"socialize-api.topolo.app"
|
|
1319
|
-
],
|
|
1267
|
+
"apiUrl": null,
|
|
1268
|
+
"domains": [],
|
|
1320
1269
|
"routes": [],
|
|
1321
|
-
"serviceId":
|
|
1270
|
+
"serviceId": null
|
|
1322
1271
|
},
|
|
1323
1272
|
{
|
|
1324
1273
|
"name": "topolo-socialize-scheduler",
|
|
@@ -1519,12 +1468,10 @@ export const APPLICATIONS = {
|
|
|
1519
1468
|
{
|
|
1520
1469
|
"name": "topolo-web-api",
|
|
1521
1470
|
"kind": "worker",
|
|
1522
|
-
"apiUrl":
|
|
1523
|
-
"domains": [
|
|
1524
|
-
"web-api.topolo.app"
|
|
1525
|
-
],
|
|
1471
|
+
"apiUrl": null,
|
|
1472
|
+
"domains": [],
|
|
1526
1473
|
"routes": [],
|
|
1527
|
-
"serviceId":
|
|
1474
|
+
"serviceId": null
|
|
1528
1475
|
},
|
|
1529
1476
|
{
|
|
1530
1477
|
"name": "topolo-web-runtime",
|
|
@@ -1547,7 +1494,7 @@ export const APPLICATIONS = {
|
|
|
1547
1494
|
"web.topolo.app"
|
|
1548
1495
|
],
|
|
1549
1496
|
"routes": [],
|
|
1550
|
-
"serviceId":
|
|
1497
|
+
"serviceId": "web"
|
|
1551
1498
|
}
|
|
1552
1499
|
]
|
|
1553
1500
|
}
|
package/src/client.test.ts
CHANGED
|
@@ -143,6 +143,40 @@ describe('TopoloClient.request', () => {
|
|
|
143
143
|
expect(headers.get('Authorization')).toBeNull();
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
it('adds canonical resource headers for resource-scoped API keys', async () => {
|
|
147
|
+
const { impl, calls } = stubFetch([MAIL_CATALOG, { body: { ok: true } }]);
|
|
148
|
+
const client = new TopoloClient({
|
|
149
|
+
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
150
|
+
agent: AGENT,
|
|
151
|
+
fetch: impl,
|
|
152
|
+
});
|
|
153
|
+
await client.request({
|
|
154
|
+
service: 'mail',
|
|
155
|
+
path: '/api/mail/mailboxes/mbx_1/messages',
|
|
156
|
+
resource: { resourceType: 'mailbox', resourceId: 'mbx_1' },
|
|
157
|
+
});
|
|
158
|
+
const headers = new Headers(requestCalls(calls)[0]!.init.headers);
|
|
159
|
+
expect(headers.get('X-Topolo-Resource-Type')).toBe('mailbox');
|
|
160
|
+
expect(headers.get('X-Topolo-Resource-ID')).toBe('mbx_1');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('passes arbitrary cataloged workspace aliases through resource headers', async () => {
|
|
164
|
+
const { impl, calls } = stubFetch([MAIL_CATALOG, { body: { ok: true } }]);
|
|
165
|
+
const client = new TopoloClient({
|
|
166
|
+
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
167
|
+
agent: AGENT,
|
|
168
|
+
fetch: impl,
|
|
169
|
+
});
|
|
170
|
+
await client.request({
|
|
171
|
+
service: 'mail',
|
|
172
|
+
path: '/api/example',
|
|
173
|
+
resource: { resourceType: 'studio_portfolio', resourceId: 'portfolio_1' },
|
|
174
|
+
});
|
|
175
|
+
const headers = new Headers(requestCalls(calls)[0]!.init.headers);
|
|
176
|
+
expect(headers.get('X-Topolo-Resource-Type')).toBe('studio_portfolio');
|
|
177
|
+
expect(headers.get('X-Topolo-Resource-ID')).toBe('portfolio_1');
|
|
178
|
+
});
|
|
179
|
+
|
|
146
180
|
it('refuses mutating requests without confirm', async () => {
|
|
147
181
|
const { impl } = stubFetch([]);
|
|
148
182
|
const client = new TopoloClient({
|
|
@@ -458,6 +492,26 @@ describe('TopoloClient.listServices', () => {
|
|
|
458
492
|
status: 'active',
|
|
459
493
|
surface_kind: 'application',
|
|
460
494
|
launchable: true,
|
|
495
|
+
api_key_resource: {
|
|
496
|
+
resourceType: 'brand',
|
|
497
|
+
label: 'Brands',
|
|
498
|
+
emptyLabel: 'All brands in the organization',
|
|
499
|
+
resourceConcept: 'workspace',
|
|
500
|
+
canonicalResourceType: 'workspace',
|
|
501
|
+
defaultEntitlement: 'enterprise',
|
|
502
|
+
exceptionPolicy: 'app_or_organization',
|
|
503
|
+
},
|
|
504
|
+
api_key_resources: [
|
|
505
|
+
{
|
|
506
|
+
resourceType: 'brand',
|
|
507
|
+
label: 'Brands',
|
|
508
|
+
emptyLabel: 'All brands in the organization',
|
|
509
|
+
resourceConcept: 'workspace',
|
|
510
|
+
canonicalResourceType: 'workspace',
|
|
511
|
+
defaultEntitlement: 'enterprise',
|
|
512
|
+
exceptionPolicy: 'app_or_organization',
|
|
513
|
+
},
|
|
514
|
+
],
|
|
461
515
|
granted_permissions: ['srv_wRCnLLbtvmVT.dashboard:read'],
|
|
462
516
|
},
|
|
463
517
|
],
|
|
@@ -483,6 +537,26 @@ describe('TopoloClient.listServices', () => {
|
|
|
483
537
|
status: 'active',
|
|
484
538
|
surfaceKind: 'application',
|
|
485
539
|
launchable: true,
|
|
540
|
+
apiKeyResource: {
|
|
541
|
+
resourceType: 'brand',
|
|
542
|
+
label: 'Brands',
|
|
543
|
+
emptyLabel: 'All brands in the organization',
|
|
544
|
+
resourceConcept: 'workspace',
|
|
545
|
+
canonicalResourceType: 'workspace',
|
|
546
|
+
defaultEntitlement: 'enterprise',
|
|
547
|
+
exceptionPolicy: 'app_or_organization',
|
|
548
|
+
},
|
|
549
|
+
apiKeyResources: [
|
|
550
|
+
{
|
|
551
|
+
resourceType: 'brand',
|
|
552
|
+
label: 'Brands',
|
|
553
|
+
emptyLabel: 'All brands in the organization',
|
|
554
|
+
resourceConcept: 'workspace',
|
|
555
|
+
canonicalResourceType: 'workspace',
|
|
556
|
+
defaultEntitlement: 'enterprise',
|
|
557
|
+
exceptionPolicy: 'app_or_organization',
|
|
558
|
+
},
|
|
559
|
+
],
|
|
486
560
|
permissions: ['srv_wRCnLLbtvmVT.dashboard:read'],
|
|
487
561
|
},
|
|
488
562
|
]);
|
package/src/client.ts
CHANGED
|
@@ -76,6 +76,11 @@ export interface RequestOptions {
|
|
|
76
76
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
77
77
|
query?: Record<string, string | number | boolean | undefined>;
|
|
78
78
|
body?: unknown;
|
|
79
|
+
/** Workspace-boundary resource context for resource-scoped API keys. */
|
|
80
|
+
resource?: {
|
|
81
|
+
resourceType: string;
|
|
82
|
+
resourceId: string;
|
|
83
|
+
};
|
|
79
84
|
/** Explicit write-acknowledgement for mutating calls. */
|
|
80
85
|
confirm?: boolean;
|
|
81
86
|
/** Extra headers appended after auth/audit headers. */
|
|
@@ -86,6 +91,8 @@ export interface RequestOptions {
|
|
|
86
91
|
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
87
92
|
const TOPOLO_ACTION_ID_HEADER = 'X-Topolo-Action-Id';
|
|
88
93
|
const TOPOLO_ACTION_NAME_HEADER = 'X-Topolo-Action-Name';
|
|
94
|
+
const TOPOLO_RESOURCE_TYPE_HEADER = 'X-Topolo-Resource-Type';
|
|
95
|
+
const TOPOLO_RESOURCE_ID_HEADER = 'X-Topolo-Resource-ID';
|
|
89
96
|
|
|
90
97
|
const POSITIVE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
91
98
|
const NEGATIVE_CACHE_TTL_MS = 60 * 1000; // 1 minute
|
|
@@ -112,6 +119,18 @@ export interface TopoloServiceCatalogEntry {
|
|
|
112
119
|
surfaceKind: string | null;
|
|
113
120
|
launchable: boolean;
|
|
114
121
|
permissions: string[];
|
|
122
|
+
apiKeyResource: TopoloApiKeyResourceMetadata | null;
|
|
123
|
+
apiKeyResources: TopoloApiKeyResourceMetadata[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface TopoloApiKeyResourceMetadata {
|
|
127
|
+
resourceType: string;
|
|
128
|
+
label: string;
|
|
129
|
+
emptyLabel: string;
|
|
130
|
+
resourceConcept: 'workspace';
|
|
131
|
+
canonicalResourceType: 'workspace';
|
|
132
|
+
defaultEntitlement: 'enterprise';
|
|
133
|
+
exceptionPolicy: 'app_or_organization';
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
export interface TopoloActionCatalogEntry {
|
|
@@ -222,6 +241,18 @@ export class TopoloClient {
|
|
|
222
241
|
if (opts.headers) {
|
|
223
242
|
for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
|
|
224
243
|
}
|
|
244
|
+
if (opts.resource) {
|
|
245
|
+
const resourceType = opts.resource.resourceType.trim();
|
|
246
|
+
const resourceId = opts.resource.resourceId.trim();
|
|
247
|
+
if (!resourceType || !resourceId) {
|
|
248
|
+
throw new TopoloSdkError(
|
|
249
|
+
'invalid_resource_context',
|
|
250
|
+
'Both resource.resourceType and resource.resourceId are required when resource context is provided.',
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
|
|
254
|
+
headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
|
|
255
|
+
}
|
|
225
256
|
|
|
226
257
|
const controller = new AbortController();
|
|
227
258
|
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
@@ -447,6 +478,18 @@ export class TopoloClient {
|
|
|
447
478
|
? s.granted_permissions
|
|
448
479
|
.filter((permission): permission is string => typeof permission === 'string' && permission.trim().length > 0)
|
|
449
480
|
: [];
|
|
481
|
+
const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
|
|
482
|
+
s.api_key_resources ?? s.apiKeyResources,
|
|
483
|
+
);
|
|
484
|
+
const apiKeyResource =
|
|
485
|
+
normalizeApiKeyResourceMetadata(s.api_key_resource ?? s.apiKeyResource) ??
|
|
486
|
+
catalogApiKeyResources[0] ??
|
|
487
|
+
null;
|
|
488
|
+
const apiKeyResources = catalogApiKeyResources.length > 0
|
|
489
|
+
? catalogApiKeyResources
|
|
490
|
+
: apiKeyResource
|
|
491
|
+
? [apiKeyResource]
|
|
492
|
+
: [];
|
|
450
493
|
entries.push({
|
|
451
494
|
serviceId,
|
|
452
495
|
slug: typeof s.app_slug === 'string' ? s.app_slug : null,
|
|
@@ -470,6 +513,8 @@ export class TopoloClient {
|
|
|
470
513
|
typeof s.surface_kind === 'string' ? s.surface_kind : null,
|
|
471
514
|
launchable: s.launchable === true,
|
|
472
515
|
permissions,
|
|
516
|
+
apiKeyResource,
|
|
517
|
+
apiKeyResources,
|
|
473
518
|
});
|
|
474
519
|
}
|
|
475
520
|
this.serviceCatalogCache = {
|
|
@@ -577,6 +622,8 @@ export class TopoloClient {
|
|
|
577
622
|
surfaceKind: 'application',
|
|
578
623
|
launchable: Boolean(app.productionUrl),
|
|
579
624
|
permissions: [],
|
|
625
|
+
apiKeyResource: null,
|
|
626
|
+
apiKeyResources: [],
|
|
580
627
|
}));
|
|
581
628
|
}
|
|
582
629
|
|
|
@@ -640,6 +687,60 @@ function resolveConfiguredServiceId(
|
|
|
640
687
|
return null;
|
|
641
688
|
}
|
|
642
689
|
|
|
690
|
+
function normalizeApiKeyResourceMetadata(input: unknown): TopoloApiKeyResourceMetadata | null {
|
|
691
|
+
if (!isRecord(input)) return null;
|
|
692
|
+
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
693
|
+
const label = readString(input.label);
|
|
694
|
+
const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
|
|
695
|
+
const resourceConcept = readString(input.resourceConcept ?? input.resource_concept);
|
|
696
|
+
const canonicalResourceType = readString(input.canonicalResourceType ?? input.canonical_resource_type);
|
|
697
|
+
const defaultEntitlement = readString(input.defaultEntitlement ?? input.default_entitlement);
|
|
698
|
+
const exceptionPolicy = readString(input.exceptionPolicy ?? input.exception_policy);
|
|
699
|
+
|
|
700
|
+
if (
|
|
701
|
+
!resourceType ||
|
|
702
|
+
!label ||
|
|
703
|
+
!emptyLabel ||
|
|
704
|
+
resourceConcept !== 'workspace' ||
|
|
705
|
+
canonicalResourceType !== 'workspace' ||
|
|
706
|
+
defaultEntitlement !== 'enterprise' ||
|
|
707
|
+
exceptionPolicy !== 'app_or_organization'
|
|
708
|
+
) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
resourceType,
|
|
714
|
+
label,
|
|
715
|
+
emptyLabel,
|
|
716
|
+
resourceConcept,
|
|
717
|
+
canonicalResourceType,
|
|
718
|
+
defaultEntitlement,
|
|
719
|
+
exceptionPolicy,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function normalizeApiKeyResourceMetadataList(input: unknown): TopoloApiKeyResourceMetadata[] {
|
|
724
|
+
if (!Array.isArray(input)) return [];
|
|
725
|
+
const resources: TopoloApiKeyResourceMetadata[] = [];
|
|
726
|
+
const seenTypes = new Set<string>();
|
|
727
|
+
for (const item of input) {
|
|
728
|
+
const resource = normalizeApiKeyResourceMetadata(item);
|
|
729
|
+
if (!resource || seenTypes.has(resource.resourceType)) continue;
|
|
730
|
+
seenTypes.add(resource.resourceType);
|
|
731
|
+
resources.push(resource);
|
|
732
|
+
}
|
|
733
|
+
return resources;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function readString(value: unknown): string {
|
|
737
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
741
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
742
|
+
}
|
|
743
|
+
|
|
643
744
|
function resolveConfiguredServiceUrl(
|
|
644
745
|
slug: string,
|
|
645
746
|
serviceUrls?: Record<string, string>,
|