@positronic/spec 0.0.67 → 0.0.69

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.
@@ -0,0 +1,756 @@
1
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
2
+ try {
3
+ var info = gen[key](arg);
4
+ var value = info.value;
5
+ } catch (error) {
6
+ reject(error);
7
+ return;
8
+ }
9
+ if (info.done) {
10
+ resolve(value);
11
+ } else {
12
+ Promise.resolve(value).then(_next, _throw);
13
+ }
14
+ }
15
+ function _async_to_generator(fn) {
16
+ return function() {
17
+ var self = this, args = arguments;
18
+ return new Promise(function(resolve, reject) {
19
+ var gen = fn.apply(self, args);
20
+ function _next(value) {
21
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
22
+ }
23
+ function _throw(err) {
24
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
25
+ }
26
+ _next(undefined);
27
+ });
28
+ };
29
+ }
30
+ function _ts_generator(thisArg, body) {
31
+ var f, y, t, _ = {
32
+ label: 0,
33
+ sent: function() {
34
+ if (t[0] & 1) throw t[1];
35
+ return t[1];
36
+ },
37
+ trys: [],
38
+ ops: []
39
+ }, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
40
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() {
41
+ return this;
42
+ }), g;
43
+ function verb(n) {
44
+ return function(v) {
45
+ return step([
46
+ n,
47
+ v
48
+ ]);
49
+ };
50
+ }
51
+ function step(op) {
52
+ if (f) throw new TypeError("Generator is already executing.");
53
+ while(g && (g = 0, op[0] && (_ = 0)), _)try {
54
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
55
+ if (y = 0, t) op = [
56
+ op[0] & 2,
57
+ t.value
58
+ ];
59
+ switch(op[0]){
60
+ case 0:
61
+ case 1:
62
+ t = op;
63
+ break;
64
+ case 4:
65
+ _.label++;
66
+ return {
67
+ value: op[1],
68
+ done: false
69
+ };
70
+ case 5:
71
+ _.label++;
72
+ y = op[1];
73
+ op = [
74
+ 0
75
+ ];
76
+ continue;
77
+ case 7:
78
+ op = _.ops.pop();
79
+ _.trys.pop();
80
+ continue;
81
+ default:
82
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
83
+ _ = 0;
84
+ continue;
85
+ }
86
+ if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
87
+ _.label = op[1];
88
+ break;
89
+ }
90
+ if (op[0] === 6 && _.label < t[1]) {
91
+ _.label = t[1];
92
+ t = op;
93
+ break;
94
+ }
95
+ if (t && _.label < t[2]) {
96
+ _.label = t[2];
97
+ _.ops.push(op);
98
+ break;
99
+ }
100
+ if (t[2]) _.ops.pop();
101
+ _.trys.pop();
102
+ continue;
103
+ }
104
+ op = body.call(thisArg, _);
105
+ } catch (e) {
106
+ op = [
107
+ 6,
108
+ e
109
+ ];
110
+ y = 0;
111
+ } finally{
112
+ f = t = 0;
113
+ }
114
+ if (op[0] & 5) throw op[1];
115
+ return {
116
+ value: op[0] ? op[1] : void 0,
117
+ done: true
118
+ };
119
+ }
120
+ }
121
+ import { BRAIN_EVENTS } from '@positronic/core';
122
+ import { startBrainRun, readSseUntil } from './helpers.js';
123
+ /**
124
+ * Helper: start a brain run as a specific user's fetch, wait for completion,
125
+ * and return the brainRunId.
126
+ */ function runBrainAndWait(fetchFn, brainIdentifier) {
127
+ return _async_to_generator(function() {
128
+ var brainRunId, watchResponse, error;
129
+ return _ts_generator(this, function(_state) {
130
+ switch(_state.label){
131
+ case 0:
132
+ _state.trys.push([
133
+ 0,
134
+ 4,
135
+ ,
136
+ 5
137
+ ]);
138
+ return [
139
+ 4,
140
+ startBrainRun(fetchFn, brainIdentifier)
141
+ ];
142
+ case 1:
143
+ brainRunId = _state.sent();
144
+ if (!brainRunId) return [
145
+ 2,
146
+ null
147
+ ];
148
+ return [
149
+ 4,
150
+ fetchFn(new Request("http://example.com/brains/runs/".concat(brainRunId, "/watch")))
151
+ ];
152
+ case 2:
153
+ watchResponse = _state.sent();
154
+ if (!watchResponse.ok || !watchResponse.body) {
155
+ console.error("GET /brains/runs/".concat(brainRunId, "/watch returned ").concat(watchResponse.status));
156
+ return [
157
+ 2,
158
+ null
159
+ ];
160
+ }
161
+ return [
162
+ 4,
163
+ readSseUntil(watchResponse.body, function(event) {
164
+ return event.type === BRAIN_EVENTS.COMPLETE || event.type === BRAIN_EVENTS.ERROR;
165
+ })
166
+ ];
167
+ case 3:
168
+ _state.sent();
169
+ return [
170
+ 2,
171
+ brainRunId
172
+ ];
173
+ case 4:
174
+ error = _state.sent();
175
+ console.error('Failed to run brain and wait:', error);
176
+ return [
177
+ 2,
178
+ null
179
+ ];
180
+ case 5:
181
+ return [
182
+ 2
183
+ ];
184
+ }
185
+ });
186
+ })();
187
+ }
188
+ export var scoping = {
189
+ brainRunIsolation: /**
190
+ * Test that brain runs are isolated between users.
191
+ *
192
+ * userA creates a run and waits for completion. Then:
193
+ * - userB gets 404 on GET /runs/:runId
194
+ * - root gets 200 on GET /runs/:runId
195
+ * - userA's history has the run
196
+ * - userB's history is empty
197
+ */ function brainRunIsolation(rootFetch, fetchFactory, brainIdentifier) {
198
+ return _async_to_generator(function() {
199
+ var userA, userB, brainRunId, responseB, responseRoot, rootRun, historyA, historyDataA, historyB, historyDataB, error;
200
+ return _ts_generator(this, function(_state) {
201
+ switch(_state.label){
202
+ case 0:
203
+ _state.trys.push([
204
+ 0,
205
+ 11,
206
+ ,
207
+ 12
208
+ ]);
209
+ return [
210
+ 4,
211
+ fetchFactory('scoping-alice-run')
212
+ ];
213
+ case 1:
214
+ userA = _state.sent();
215
+ return [
216
+ 4,
217
+ fetchFactory('scoping-bob-run')
218
+ ];
219
+ case 2:
220
+ userB = _state.sent();
221
+ return [
222
+ 4,
223
+ runBrainAndWait(userA.fetch, brainIdentifier)
224
+ ];
225
+ case 3:
226
+ brainRunId = _state.sent();
227
+ if (!brainRunId) {
228
+ console.error('Failed to create and complete brain run as userA');
229
+ return [
230
+ 2,
231
+ false
232
+ ];
233
+ }
234
+ return [
235
+ 4,
236
+ userB.fetch(new Request("http://example.com/brains/runs/".concat(brainRunId)))
237
+ ];
238
+ case 4:
239
+ responseB = _state.sent();
240
+ if (responseB.status !== 404) {
241
+ console.error("Expected userB to get 404 for userA's run, got ".concat(responseB.status));
242
+ return [
243
+ 2,
244
+ false
245
+ ];
246
+ }
247
+ return [
248
+ 4,
249
+ rootFetch(new Request("http://example.com/brains/runs/".concat(brainRunId)))
250
+ ];
251
+ case 5:
252
+ responseRoot = _state.sent();
253
+ if (responseRoot.status !== 200) {
254
+ console.error("Expected root to get 200 for userA's run, got ".concat(responseRoot.status));
255
+ return [
256
+ 2,
257
+ false
258
+ ];
259
+ }
260
+ return [
261
+ 4,
262
+ responseRoot.json()
263
+ ];
264
+ case 6:
265
+ rootRun = _state.sent();
266
+ if (rootRun.brainRunId !== brainRunId) {
267
+ console.error("Root returned wrong brainRunId: expected ".concat(brainRunId, ", got ").concat(rootRun.brainRunId));
268
+ return [
269
+ 2,
270
+ false
271
+ ];
272
+ }
273
+ return [
274
+ 4,
275
+ userA.fetch(new Request("http://example.com/brains/".concat(encodeURIComponent(brainIdentifier), "/history?limit=50")))
276
+ ];
277
+ case 7:
278
+ historyA = _state.sent();
279
+ if (historyA.status !== 200) {
280
+ console.error("Expected userA history to return 200, got ".concat(historyA.status));
281
+ return [
282
+ 2,
283
+ false
284
+ ];
285
+ }
286
+ return [
287
+ 4,
288
+ historyA.json()
289
+ ];
290
+ case 8:
291
+ historyDataA = _state.sent();
292
+ if (historyDataA.runs.length === 0) {
293
+ console.error("userA's history is empty but should have the run");
294
+ return [
295
+ 2,
296
+ false
297
+ ];
298
+ }
299
+ return [
300
+ 4,
301
+ userB.fetch(new Request("http://example.com/brains/".concat(encodeURIComponent(brainIdentifier), "/history?limit=50")))
302
+ ];
303
+ case 9:
304
+ historyB = _state.sent();
305
+ if (historyB.status !== 200) {
306
+ console.error("Expected userB history to return 200, got ".concat(historyB.status));
307
+ return [
308
+ 2,
309
+ false
310
+ ];
311
+ }
312
+ return [
313
+ 4,
314
+ historyB.json()
315
+ ];
316
+ case 10:
317
+ historyDataB = _state.sent();
318
+ if (historyDataB.runs.length !== 0) {
319
+ console.error("userB's history should be empty but has ".concat(historyDataB.runs.length, " runs"));
320
+ return [
321
+ 2,
322
+ false
323
+ ];
324
+ }
325
+ return [
326
+ 2,
327
+ true
328
+ ];
329
+ case 11:
330
+ error = _state.sent();
331
+ console.error('Failed brain run isolation spec:', error);
332
+ return [
333
+ 2,
334
+ false
335
+ ];
336
+ case 12:
337
+ return [
338
+ 2
339
+ ];
340
+ }
341
+ });
342
+ })();
343
+ },
344
+ activeRunIsolation: /**
345
+ * Test that active runs are isolated between users.
346
+ *
347
+ * userA starts a delayed brain (will remain running).
348
+ * - userA sees it in active-runs
349
+ * - userB does not see it in active-runs
350
+ */ function activeRunIsolation(rootFetch, fetchFactory, delayedBrainIdentifier) {
351
+ return _async_to_generator(function() {
352
+ var userA, userB, brainRunId, responseB, activeB, responseA, activeA, error;
353
+ return _ts_generator(this, function(_state) {
354
+ switch(_state.label){
355
+ case 0:
356
+ _state.trys.push([
357
+ 0,
358
+ 8,
359
+ ,
360
+ 9
361
+ ]);
362
+ return [
363
+ 4,
364
+ fetchFactory('scoping-alice-active')
365
+ ];
366
+ case 1:
367
+ userA = _state.sent();
368
+ return [
369
+ 4,
370
+ fetchFactory('scoping-bob-active')
371
+ ];
372
+ case 2:
373
+ userB = _state.sent();
374
+ return [
375
+ 4,
376
+ startBrainRun(userA.fetch, delayedBrainIdentifier)
377
+ ];
378
+ case 3:
379
+ brainRunId = _state.sent();
380
+ if (!brainRunId) return [
381
+ 2,
382
+ false
383
+ ];
384
+ return [
385
+ 4,
386
+ userB.fetch(new Request("http://example.com/brains/".concat(encodeURIComponent(delayedBrainIdentifier), "/active-runs")))
387
+ ];
388
+ case 4:
389
+ responseB = _state.sent();
390
+ if (responseB.status !== 200) {
391
+ console.error("Expected userB active-runs to return 200, got ".concat(responseB.status));
392
+ return [
393
+ 2,
394
+ false
395
+ ];
396
+ }
397
+ return [
398
+ 4,
399
+ responseB.json()
400
+ ];
401
+ case 5:
402
+ activeB = _state.sent();
403
+ if (activeB.runs.length !== 0) {
404
+ console.error("userB should see 0 active runs but sees ".concat(activeB.runs.length));
405
+ return [
406
+ 2,
407
+ false
408
+ ];
409
+ }
410
+ return [
411
+ 4,
412
+ userA.fetch(new Request("http://example.com/brains/".concat(encodeURIComponent(delayedBrainIdentifier), "/active-runs")))
413
+ ];
414
+ case 6:
415
+ responseA = _state.sent();
416
+ if (responseA.status !== 200) {
417
+ console.error("Expected userA active-runs to return 200, got ".concat(responseA.status));
418
+ return [
419
+ 2,
420
+ false
421
+ ];
422
+ }
423
+ return [
424
+ 4,
425
+ responseA.json()
426
+ ];
427
+ case 7:
428
+ activeA = _state.sent();
429
+ if (activeA.runs.length === 0) {
430
+ console.error('userA should see at least 1 active run but sees 0');
431
+ return [
432
+ 2,
433
+ false
434
+ ];
435
+ }
436
+ return [
437
+ 2,
438
+ true
439
+ ];
440
+ case 8:
441
+ error = _state.sent();
442
+ console.error('Failed active run isolation spec:', error);
443
+ return [
444
+ 2,
445
+ false
446
+ ];
447
+ case 9:
448
+ return [
449
+ 2
450
+ ];
451
+ }
452
+ });
453
+ })();
454
+ },
455
+ scheduleIsolation: /**
456
+ * Test that schedules are isolated between users.
457
+ *
458
+ * userA creates a schedule. Then:
459
+ * - userB can't see it in GET /schedules
460
+ * - userA can see it in GET /schedules
461
+ * - root can see it in GET /schedules
462
+ * - userB can't see runs for it in GET /schedules/runs
463
+ */ function scheduleIsolation(rootFetch, fetchFactory, brainIdentifier) {
464
+ return _async_to_generator(function() {
465
+ var userA, userB, createResponse, schedule, listResponseB, listB, listResponseA, listA, listResponseRoot, listRoot, runsResponseB, runsB, error;
466
+ return _ts_generator(this, function(_state) {
467
+ switch(_state.label){
468
+ case 0:
469
+ _state.trys.push([
470
+ 0,
471
+ 13,
472
+ ,
473
+ 14
474
+ ]);
475
+ return [
476
+ 4,
477
+ fetchFactory('scoping-alice-sched')
478
+ ];
479
+ case 1:
480
+ userA = _state.sent();
481
+ return [
482
+ 4,
483
+ fetchFactory('scoping-bob-sched')
484
+ ];
485
+ case 2:
486
+ userB = _state.sent();
487
+ return [
488
+ 4,
489
+ userA.fetch(new Request('http://example.com/brains/schedules', {
490
+ method: 'POST',
491
+ headers: {
492
+ 'Content-Type': 'application/json'
493
+ },
494
+ body: JSON.stringify({
495
+ identifier: brainIdentifier,
496
+ cronExpression: '0 6 * * *'
497
+ })
498
+ }))
499
+ ];
500
+ case 3:
501
+ createResponse = _state.sent();
502
+ if (createResponse.status !== 201) {
503
+ console.error("POST /brains/schedules returned ".concat(createResponse.status, ", expected 201"));
504
+ return [
505
+ 2,
506
+ false
507
+ ];
508
+ }
509
+ return [
510
+ 4,
511
+ createResponse.json()
512
+ ];
513
+ case 4:
514
+ schedule = _state.sent();
515
+ if (schedule.runAsUserId !== userA.userId) {
516
+ console.error("Expected runAsUserId to be '".concat(userA.userId, "', got '").concat(schedule.runAsUserId, "'"));
517
+ return [
518
+ 2,
519
+ false
520
+ ];
521
+ }
522
+ return [
523
+ 4,
524
+ userB.fetch(new Request('http://example.com/brains/schedules'))
525
+ ];
526
+ case 5:
527
+ listResponseB = _state.sent();
528
+ if (listResponseB.status !== 200) {
529
+ console.error("Expected userB schedule list to return 200, got ".concat(listResponseB.status));
530
+ return [
531
+ 2,
532
+ false
533
+ ];
534
+ }
535
+ return [
536
+ 4,
537
+ listResponseB.json()
538
+ ];
539
+ case 6:
540
+ listB = _state.sent();
541
+ if (listB.schedules.some(function(s) {
542
+ return s.id === schedule.id;
543
+ })) {
544
+ console.error("userB should not see userA's schedule in list");
545
+ return [
546
+ 2,
547
+ false
548
+ ];
549
+ }
550
+ return [
551
+ 4,
552
+ userA.fetch(new Request('http://example.com/brains/schedules'))
553
+ ];
554
+ case 7:
555
+ listResponseA = _state.sent();
556
+ if (listResponseA.status !== 200) {
557
+ console.error("Expected userA schedule list to return 200, got ".concat(listResponseA.status));
558
+ return [
559
+ 2,
560
+ false
561
+ ];
562
+ }
563
+ return [
564
+ 4,
565
+ listResponseA.json()
566
+ ];
567
+ case 8:
568
+ listA = _state.sent();
569
+ if (!listA.schedules.some(function(s) {
570
+ return s.id === schedule.id;
571
+ })) {
572
+ console.error("userA should see their own schedule in list");
573
+ return [
574
+ 2,
575
+ false
576
+ ];
577
+ }
578
+ return [
579
+ 4,
580
+ rootFetch(new Request('http://example.com/brains/schedules'))
581
+ ];
582
+ case 9:
583
+ listResponseRoot = _state.sent();
584
+ if (listResponseRoot.status !== 200) {
585
+ console.error("Expected root schedule list to return 200, got ".concat(listResponseRoot.status));
586
+ return [
587
+ 2,
588
+ false
589
+ ];
590
+ }
591
+ return [
592
+ 4,
593
+ listResponseRoot.json()
594
+ ];
595
+ case 10:
596
+ listRoot = _state.sent();
597
+ if (!listRoot.schedules.some(function(s) {
598
+ return s.id === schedule.id;
599
+ })) {
600
+ console.error("root should see userA's schedule in list");
601
+ return [
602
+ 2,
603
+ false
604
+ ];
605
+ }
606
+ return [
607
+ 4,
608
+ userB.fetch(new Request("http://example.com/brains/schedules/runs?scheduleId=".concat(schedule.id)))
609
+ ];
610
+ case 11:
611
+ runsResponseB = _state.sent();
612
+ if (runsResponseB.status !== 200) {
613
+ console.error("Expected userB schedule runs to return 200, got ".concat(runsResponseB.status));
614
+ return [
615
+ 2,
616
+ false
617
+ ];
618
+ }
619
+ return [
620
+ 4,
621
+ runsResponseB.json()
622
+ ];
623
+ case 12:
624
+ runsB = _state.sent();
625
+ if (runsB.runs.length !== 0) {
626
+ console.error("userB should see 0 schedule runs but sees ".concat(runsB.runs.length));
627
+ return [
628
+ 2,
629
+ false
630
+ ];
631
+ }
632
+ return [
633
+ 2,
634
+ true
635
+ ];
636
+ case 13:
637
+ error = _state.sent();
638
+ console.error('Failed schedule isolation spec:', error);
639
+ return [
640
+ 2,
641
+ false
642
+ ];
643
+ case 14:
644
+ return [
645
+ 2
646
+ ];
647
+ }
648
+ });
649
+ })();
650
+ },
651
+ secretsRequireRoot: /**
652
+ * Test that non-root users get 403 on secrets endpoints.
653
+ *
654
+ * A non-root user calling GET/POST/DELETE /secrets gets 403.
655
+ * (Root access to secrets is already tested by secrets.list spec.)
656
+ */ function secretsRequireRoot(rootFetch, fetchFactory) {
657
+ return _async_to_generator(function() {
658
+ var user, getResponse, getBody, postResponse, deleteResponse, error;
659
+ return _ts_generator(this, function(_state) {
660
+ switch(_state.label){
661
+ case 0:
662
+ _state.trys.push([
663
+ 0,
664
+ 6,
665
+ ,
666
+ 7
667
+ ]);
668
+ return [
669
+ 4,
670
+ fetchFactory('scoping-user-secrets')
671
+ ];
672
+ case 1:
673
+ user = _state.sent();
674
+ return [
675
+ 4,
676
+ user.fetch(new Request('http://example.com/secrets'))
677
+ ];
678
+ case 2:
679
+ getResponse = _state.sent();
680
+ if (getResponse.status !== 403) {
681
+ console.error("Expected non-root GET /secrets to return 403, got ".concat(getResponse.status));
682
+ return [
683
+ 2,
684
+ false
685
+ ];
686
+ }
687
+ return [
688
+ 4,
689
+ getResponse.json()
690
+ ];
691
+ case 3:
692
+ getBody = _state.sent();
693
+ if (getBody.error !== 'Root access required') {
694
+ console.error("Expected error 'Root access required', got '".concat(getBody.error, "'"));
695
+ return [
696
+ 2,
697
+ false
698
+ ];
699
+ }
700
+ return [
701
+ 4,
702
+ user.fetch(new Request('http://example.com/secrets', {
703
+ method: 'POST',
704
+ headers: {
705
+ 'Content-Type': 'application/json'
706
+ },
707
+ body: JSON.stringify({
708
+ name: 'MY_SECRET',
709
+ value: 'secret-value'
710
+ })
711
+ }))
712
+ ];
713
+ case 4:
714
+ postResponse = _state.sent();
715
+ if (postResponse.status !== 403) {
716
+ console.error("Expected non-root POST /secrets to return 403, got ".concat(postResponse.status));
717
+ return [
718
+ 2,
719
+ false
720
+ ];
721
+ }
722
+ return [
723
+ 4,
724
+ user.fetch(new Request('http://example.com/secrets/MY_SECRET', {
725
+ method: 'DELETE'
726
+ }))
727
+ ];
728
+ case 5:
729
+ deleteResponse = _state.sent();
730
+ if (deleteResponse.status !== 403) {
731
+ console.error("Expected non-root DELETE /secrets to return 403, got ".concat(deleteResponse.status));
732
+ return [
733
+ 2,
734
+ false
735
+ ];
736
+ }
737
+ return [
738
+ 2,
739
+ true
740
+ ];
741
+ case 6:
742
+ error = _state.sent();
743
+ console.error('Failed secrets require root spec:', error);
744
+ return [
745
+ 2,
746
+ false
747
+ ];
748
+ case 7:
749
+ return [
750
+ 2
751
+ ];
752
+ }
753
+ });
754
+ })();
755
+ }
756
+ };