@startsimpli/integrations 0.1.6 → 0.2.2
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/package.json +2 -2
- package/src/__tests__/connections.test.tsx +176 -0
- package/src/__tests__/corpus-extract.test.ts +537 -0
- package/src/__tests__/corpus-tracker.test.tsx +250 -0
- package/src/components/index.ts +1 -0
- package/src/components/pages/ConnectionsPage.tsx +229 -11
- package/src/components/pages/TrackerPage.tsx +437 -0
- package/src/components/pages/types.ts +25 -0
- package/src/corpus/client.ts +85 -4
- package/src/corpus/commitments.ts +416 -0
- package/src/corpus/entities.ts +134 -0
- package/src/corpus/extract.ts +438 -0
- package/src/corpus/fixtures.ts +176 -0
- package/src/corpus/index.ts +46 -0
- package/src/corpus/llm.ts +107 -1
- package/src/corpus/text.ts +283 -0
- package/src/corpus/types.ts +256 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/integrations",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Provider-agnostic types, API clients, hooks, and UI primitives for email + calendar integrations across StartSimpli apps.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"react-dom": "^19.0.0",
|
|
49
49
|
"typescript": "^6.0.0",
|
|
50
50
|
"vitest": "^3.0.0",
|
|
51
|
-
"@startsimpli/llm": "0.4.
|
|
51
|
+
"@startsimpli/llm": "0.4.15"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"type-check": "tsc --noEmit",
|
|
@@ -204,6 +204,7 @@ function stubApi(overrides: Partial<ConnectionsApi> = {}): ConnectionsApi {
|
|
|
204
204
|
}),
|
|
205
205
|
connectionRuns: vi.fn().mockResolvedValue({ results: [] }),
|
|
206
206
|
updateConnection: vi.fn().mockResolvedValue(CONNECTED.results[0]),
|
|
207
|
+
rotateCredential: vi.fn().mockResolvedValue(CONNECTED.results[0]),
|
|
207
208
|
...overrides,
|
|
208
209
|
} as ConnectionsApi;
|
|
209
210
|
}
|
|
@@ -383,6 +384,181 @@ describe('capabilities are the admin’s choice, and the page has to ask', () =>
|
|
|
383
384
|
});
|
|
384
385
|
});
|
|
385
386
|
|
|
387
|
+
// --------------------------------------------------------------------------
|
|
388
|
+
// a card an admin can actually maintain (bd startsim-768w.22.24)
|
|
389
|
+
// --------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* WHAT A CONNECTED CARD COULD DO BEFORE THIS: test it, discover its workflows,
|
|
393
|
+
* change its capabilities, wire and fire a target. That is the whole list. It
|
|
394
|
+
* could not have its KEY REPLACED and it could not be REMOVED — the two things
|
|
395
|
+
* an admin holding a freshly-minted API key actually needs.
|
|
396
|
+
*
|
|
397
|
+
* `POST /connections/<id>/credential/` has done a checked, non-destructive
|
|
398
|
+
* rotation since the endpoint was designed, and `DELETE /connections/<id>/` has
|
|
399
|
+
* done a transactional disconnect for just as long. `disconnect` was even
|
|
400
|
+
* declared on this page's own api contract. NOTHING RENDERED EITHER ONE. So an
|
|
401
|
+
* admin with a new key had exactly one control that looked like it would take
|
|
402
|
+
* one — the connect form — and using it hit a duplicate-credential collision
|
|
403
|
+
* that came back as a Django HTML 500 (the server half is bd
|
|
404
|
+
* startsim-768w.22.24 too). The missing button is the actual bug; the 500 is
|
|
405
|
+
* only where it surfaced.
|
|
406
|
+
*/
|
|
407
|
+
describe('replacing the key on a connection that already exists', () => {
|
|
408
|
+
async function openReplaceForm(api = stubApi()) {
|
|
409
|
+
render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
410
|
+
await screen.findByText('Ops n8n');
|
|
411
|
+
await userEvent.click(screen.getByRole('button', { name: /replace key|new key/i }));
|
|
412
|
+
return api;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
it('offers a way to replace the key on a connected card', async () => {
|
|
416
|
+
render(<ConnectionsPage api={stubApi()} foundrySlug="acme" />);
|
|
417
|
+
await screen.findByText('Ops n8n');
|
|
418
|
+
expect(
|
|
419
|
+
screen.getByRole('button', { name: /replace key|new key/i }),
|
|
420
|
+
).toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('renders the connector’s OWN credential fields, from the catalog row', async () => {
|
|
424
|
+
// Same schema-driven renderer as the connect form. A bespoke "paste your
|
|
425
|
+
// key" box would be the per-vendor branch this layer exists to refuse: the
|
|
426
|
+
// fixture connector takes a url + a secret + a select, and the replace form
|
|
427
|
+
// has to show the ones that are credentials.
|
|
428
|
+
await openReplaceForm();
|
|
429
|
+
expect(await screen.findByLabelText(/Public API key/i)).toBeInTheDocument();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('says the current key survives a bad paste, where the admin can read it', async () => {
|
|
433
|
+
// The reassurance someone pasting a production key wants, and it is TRUE:
|
|
434
|
+
// rotation runs check() against the vendor and writes only on success, so a
|
|
435
|
+
// wrong key leaves the working one live. Saying nothing here is what makes
|
|
436
|
+
// people go looking for a "safer" path — which is the connect form.
|
|
437
|
+
await openReplaceForm();
|
|
438
|
+
expect(
|
|
439
|
+
screen.getByText(/before it replaces|until the new one|only if it works|nothing changes/i),
|
|
440
|
+
).toBeInTheDocument();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('sends the pasted key to the ROTATE endpoint, not to connect', async () => {
|
|
444
|
+
const api = await openReplaceForm();
|
|
445
|
+
await userEvent.type(screen.getByLabelText(/Public API key/i), 'n_new_key_123');
|
|
446
|
+
await userEvent.click(screen.getByRole('button', { name: /^replace$|test and replace/i }));
|
|
447
|
+
|
|
448
|
+
await waitFor(() => expect(api.rotateCredential).toHaveBeenCalledWith('acme', 'c1', {
|
|
449
|
+
api_key: 'n_new_key_123',
|
|
450
|
+
}));
|
|
451
|
+
expect(api.connect).not.toHaveBeenCalled();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('reports a rejected key instead of claiming the rotation landed', async () => {
|
|
455
|
+
const rejected = Object.assign(new Error('Invalid API key format'), { status: 401 });
|
|
456
|
+
const api = stubApi({ rotateCredential: vi.fn().mockRejectedValue(rejected) });
|
|
457
|
+
await openReplaceForm(api);
|
|
458
|
+
await userEvent.type(screen.getByLabelText(/Public API key/i), 'bad');
|
|
459
|
+
await userEvent.click(screen.getByRole('button', { name: /^replace$|test and replace/i }));
|
|
460
|
+
|
|
461
|
+
expect(await screen.findByText(/Invalid API key format/)).toBeInTheDocument();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('does not offer to replace a key when manage is false', async () => {
|
|
465
|
+
const api = stubApi({
|
|
466
|
+
connections: vi.fn().mockResolvedValue({
|
|
467
|
+
...CONNECTED,
|
|
468
|
+
permissions: { view: true, manage: false, issue_tokens: false },
|
|
469
|
+
}),
|
|
470
|
+
});
|
|
471
|
+
render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
472
|
+
await screen.findByText('Ops n8n');
|
|
473
|
+
expect(
|
|
474
|
+
screen.queryByRole('button', { name: /replace key|new key/i }),
|
|
475
|
+
).not.toBeInTheDocument();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
describe('removing a connection', () => {
|
|
480
|
+
it('offers disconnect on a connected card', async () => {
|
|
481
|
+
render(<ConnectionsPage api={stubApi()} foundrySlug="acme" />);
|
|
482
|
+
await screen.findByText('Ops n8n');
|
|
483
|
+
expect(screen.getByRole('button', { name: /disconnect/i })).toBeInTheDocument();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('CONFIRMS first, and says what stops', async () => {
|
|
487
|
+
// Disconnect is not an edit. It stops the scheduled fetches, stops the
|
|
488
|
+
// outbound sends and revokes every token issued for this account, in one
|
|
489
|
+
// transaction. A single misclick doing all of that silently is not a
|
|
490
|
+
// control an admin can use with confidence.
|
|
491
|
+
const api = stubApi();
|
|
492
|
+
render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
493
|
+
await screen.findByText('Ops n8n');
|
|
494
|
+
await userEvent.click(screen.getByRole('button', { name: /^disconnect$/i }));
|
|
495
|
+
|
|
496
|
+
expect(api.disconnect).not.toHaveBeenCalled();
|
|
497
|
+
expect(screen.getByText(/stop|revoke/i)).toBeInTheDocument();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('disconnects once confirmed, and refreshes the list', async () => {
|
|
501
|
+
const api = stubApi();
|
|
502
|
+
render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
503
|
+
await screen.findByText('Ops n8n');
|
|
504
|
+
await userEvent.click(screen.getByRole('button', { name: /^disconnect$/i }));
|
|
505
|
+
await userEvent.click(screen.getByRole('button', { name: /yes, disconnect|confirm disconnect/i }));
|
|
506
|
+
|
|
507
|
+
await waitFor(() => expect(api.disconnect).toHaveBeenCalledWith('acme', 'c1'));
|
|
508
|
+
// Two loads: the mount, and the one after the card changed.
|
|
509
|
+
await waitFor(() => expect(
|
|
510
|
+
(api.connections as ReturnType<typeof vi.fn>).mock.calls.length,
|
|
511
|
+
).toBeGreaterThan(1));
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('lets an admin back out of the confirmation', async () => {
|
|
515
|
+
const api = stubApi();
|
|
516
|
+
render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
517
|
+
await screen.findByText('Ops n8n');
|
|
518
|
+
await userEvent.click(screen.getByRole('button', { name: /^disconnect$/i }));
|
|
519
|
+
await userEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
|
520
|
+
|
|
521
|
+
expect(api.disconnect).not.toHaveBeenCalled();
|
|
522
|
+
expect(
|
|
523
|
+
screen.queryByRole('button', { name: /yes, disconnect|confirm disconnect/i }),
|
|
524
|
+
).not.toBeInTheDocument();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('does not offer disconnect when manage is false', async () => {
|
|
528
|
+
const api = stubApi({
|
|
529
|
+
connections: vi.fn().mockResolvedValue({
|
|
530
|
+
...CONNECTED,
|
|
531
|
+
permissions: { view: true, manage: false, issue_tokens: false },
|
|
532
|
+
}),
|
|
533
|
+
});
|
|
534
|
+
render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
535
|
+
await screen.findByText('Ops n8n');
|
|
536
|
+
expect(screen.queryByRole('button', { name: /disconnect/i })).not.toBeInTheDocument();
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
describe('the connect form knows what is already connected', () => {
|
|
541
|
+
it('says so, and names the card, before an admin fills the form in again', async () => {
|
|
542
|
+
// The server now answers a duplicate connect with a 409 that names the card
|
|
543
|
+
// (bd startsim-768w.22.24) instead of an IntegrityError. Saying it HERE
|
|
544
|
+
// costs a round trip less and, more to the point, catches the admin before
|
|
545
|
+
// they have pasted a production key into the wrong form.
|
|
546
|
+
render(<ConnectionsPage api={stubApi()} foundrySlug="acme" />);
|
|
547
|
+
await userEvent.click(await screen.findByRole('button', { name: /^connect$/i }));
|
|
548
|
+
expect(screen.getByText(/already connected/i)).toBeInTheDocument();
|
|
549
|
+
expect(screen.getAllByText(/Ops n8n/).length).toBeGreaterThan(1);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('still allows a SECOND account on the same vendor', async () => {
|
|
553
|
+
// Two workspaces on one vendor are two accounts and two cards — the server
|
|
554
|
+
// says so in `Connection`'s own constraint. The warning is a warning, never
|
|
555
|
+
// a lock.
|
|
556
|
+
render(<ConnectionsPage api={stubApi()} foundrySlug="acme" />);
|
|
557
|
+
await userEvent.click(await screen.findByRole('button', { name: /^connect$/i }));
|
|
558
|
+
expect(screen.getByRole('button', { name: /test and save/i })).toBeInTheDocument();
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
386
562
|
describe('the catalog envelope, as the server actually ships it', () => {
|
|
387
563
|
// REGRESSION. The first browser drive of this page died on
|
|
388
564
|
// `(entry.credential_schema ?? []) is not iterable`, because these fixtures
|