@ascendenceai/cortena-extensions-e2e-kit 0.1.0

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.
Files changed (42) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +126 -0
  3. package/dist/config.d.ts +59 -0
  4. package/dist/config.js +63 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/env.d.ts +43 -0
  7. package/dist/env.js +67 -0
  8. package/dist/env.js.map +1 -0
  9. package/dist/fixtures/consent.d.ts +107 -0
  10. package/dist/fixtures/consent.js +220 -0
  11. package/dist/fixtures/consent.js.map +1 -0
  12. package/dist/fixtures/embedded.d.ts +47 -0
  13. package/dist/fixtures/embedded.js +60 -0
  14. package/dist/fixtures/embedded.js.map +1 -0
  15. package/dist/fixtures/list-state.d.ts +43 -0
  16. package/dist/fixtures/list-state.js +58 -0
  17. package/dist/fixtures/list-state.js.map +1 -0
  18. package/dist/fixtures/popup.d.ts +44 -0
  19. package/dist/fixtures/popup.js +81 -0
  20. package/dist/fixtures/popup.js.map +1 -0
  21. package/dist/fixtures/shell.d.ts +63 -0
  22. package/dist/fixtures/shell.js +96 -0
  23. package/dist/fixtures/shell.js.map +1 -0
  24. package/dist/fixtures/sign-in.d.ts +59 -0
  25. package/dist/fixtures/sign-in.js +115 -0
  26. package/dist/fixtures/sign-in.js.map +1 -0
  27. package/dist/fixtures/theme.d.ts +42 -0
  28. package/dist/fixtures/theme.js +79 -0
  29. package/dist/fixtures/theme.js.map +1 -0
  30. package/dist/index.d.ts +34 -0
  31. package/dist/index.js +25 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/locators.d.ts +210 -0
  34. package/dist/locators.js +212 -0
  35. package/dist/locators.js.map +1 -0
  36. package/dist/protocol/suite.d.ts +31 -0
  37. package/dist/protocol/suite.js +196 -0
  38. package/dist/protocol/suite.js.map +1 -0
  39. package/dist/protocol/types.d.ts +61 -0
  40. package/dist/protocol/types.js +4 -0
  41. package/dist/protocol/types.js.map +1 -0
  42. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 Ascendence AI Technology Pvt Ltd. All rights reserved.
2
+
3
+ This package is published for use with the Cortena platform by licensed Cortena
4
+ customers and partners only. No permission is granted to copy, modify,
5
+ redistribute or use it outside a Cortena deployment.
6
+
7
+ Provided as is, without warranty.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # @ascendenceai/cortena-extensions-e2e-kit
2
+
3
+ The shared Playwright layer-3 kit — HOW-TO §23.4, audit rule P-36.
4
+
5
+ §23.4 names the same six end-to-end flows for every extension, and none of them
6
+ exist until something is deployed: the login page, the extension inside the
7
+ CortenaWeb frame, the MCP consent page, the agent pop-up, UI state, and shell
8
+ chrome — each in light **and** dark. Written per extension that is six copies of
9
+ one sign-in helper, one theme switcher and one way of finding the sidebar, and
10
+ drift on the single layer that is supposed to prove the fleet looks the same.
11
+ Six of seven extensions therefore had no suite at all.
12
+
13
+ Here it is written once. An extension supplies a config file and six values.
14
+
15
+ ## What an extension supplies
16
+
17
+ Two files, both beside the extension root so `extension-ci.yml`'s end-to-end job
18
+ finds them (`working-directory: extensions/<name>`).
19
+
20
+ ### `extensions/<name>/playwright.config.ts`
21
+
22
+ ```ts
23
+ import { protocolConfig } from '@ascendenceai/cortena-extensions-e2e-kit/config';
24
+
25
+ export default protocolConfig({
26
+ host: 'https://test.<name>.ascendenceai.com',
27
+ suitePackage: '@cortena-extensions/<name>-e2e',
28
+ });
29
+ ```
30
+
31
+ `retries: 0`, both themes as two projects, no web server and the Assure reporter
32
+ are all the kit's; they are not options because each one is a rule (§23.4,
33
+ §23.7). `CORTENA_E2E_BASE_URL` overrides `host` for a branch deployment.
34
+
35
+ ### `extensions/<name>/e2e/protocol.spec.ts`
36
+
37
+ ```ts
38
+ defineProtocolSuite({
39
+ host, listRoute, filter, detail, agentSlug, helpTopic, expects, cases,
40
+ });
41
+ ```
42
+
43
+ | Value | What it is |
44
+ | ------------ | ---------- |
45
+ | `host` | The deployed test host. |
46
+ | `listRoute` | The extension's first list route, without a query. |
47
+ | `filter` | `{ param, value }` — one filter on that list, as the **query parameter** that holds it. If the filter is not in the URL, §9 is not satisfied and the spec says so. |
48
+ | `detail` | `{ open(page), urlPattern, close(page) }` — how a row is opened, the URL it must be at, and how it is closed. |
49
+ | `agentSlug` | The AgentTemplate slug, which is also the extension id and the mark id (§19.1). |
50
+ | `helpTopic` | Words the help panel must contain, from the extension's own functional document (§13). |
51
+ | `expects` | `brandName`, `navItems`, `menuItems?`, `listHeading`, `agentPrompt`, `a2uiPrompt?`, `consentScopes?`. |
52
+ | `cases` | The six `assureCase({ … })` literals. See below. |
53
+
54
+ ## Why the six cases are written in the extension's file
55
+
56
+ `assure-sync-cases` reads `assureCase({ … })` out of the source **without
57
+ running it** — it parses the object literal so a case for a broken screen is
58
+ still written down — and it skips `node_modules`. A case built inside this
59
+ package is therefore a case the catalogue never hears about, and a spec with no
60
+ case records no result. So the literals live in the extension's own
61
+ `e2e/protocol.spec.ts`, where the scanner walks; the *behaviour* lives here,
62
+ where it is written once. `defineProtocolSuite` refuses to define anything if
63
+ one of the six is missing. The generator emits them; an adopter copies them.
64
+
65
+ ## The fixtures, on their own
66
+
67
+ | Fixture | What it does |
68
+ | ------- | ------------ |
69
+ | `signIn(page, { route, useForm })` | The organisation step, then the credentials step, against the test cortena-auth. With `CORTENA_E2E_TOKEN` and no `useForm`, the token handoff instead. |
70
+ | `embedded(page, { route })` | The CortenaWeb launch: `?token=…&refreshToken=none`, and the assertion that no login screen appears. |
71
+ | `theme(page, mode, { path })` | `attribute` writes the stored choice and asserts `data-theme`; `os` clears it and emulates `prefers-color-scheme`, which is the path a first visit takes. |
72
+ | `shell(page)` | Every part of `AppShell` as locators, plus `openAvatarMenu`, `openHelp`, `openNavDrawer`. |
73
+ | `popup(page)` | `open`, `ask`, `awaitStream`, and the A2UI block. `expectSessionResumes` is the reload half. |
74
+ | `listState(page, { route, filter, detail })` | The five-way check of §9: detail, close, Back, reload, and the URL in a new page. |
75
+ | `consent(page, { host, scopes })` | The MCP consent page. The resource, the authorisation server and the scopes are **discovered** from the extension's own `/.well-known/oauth-protected-resource` (RFC 9728), a client is registered dynamically, and the PKCE authorisation request is opened — the sequence an MCP client performs. cortena-auth's hosted page is located by its form, because it is not built from cortena-ui. Asserted and then **refused**: a suite that presses Allow every run leaves grants nobody revokes. |
76
+
77
+ ## Credentials
78
+
79
+ Read from the environment and never committed:
80
+
81
+ ```
82
+ CORTENA_E2E_ORG_ID the dedicated test org (required)
83
+ CORTENA_E2E_TOKEN a pre-minted token, or
84
+ CORTENA_E2E_USER_EMAIL the test user and
85
+ CORTENA_E2E_USER_PASSWORD its password
86
+ ```
87
+
88
+ `CORTENA_E2E_USER_EMAIL` and `CORTENA_E2E_USER_PASSWORD` are not optional in
89
+ practice: a pre-minted token is a session in the *extension*, and two of the six
90
+ specs are about screens that are not the extension's — the login page, and the
91
+ MCP consent page on cortena-auth's own host. Both need a person to sign in.
92
+ Given only a token, the login spec skips and the consent spec fails saying so.
93
+
94
+ Optional: `CORTENA_E2E_BASE_URL` to point at a branch deployment, and
95
+ `CORTENA_E2E_MCP_CLIENT_ID` for an environment that forbids dynamic client
96
+ registration.
97
+
98
+ In CI these are repository secrets. `extension-ci.yml`'s `e2e-credentials` job
99
+ checks for them and **skips** the end-to-end job when they are absent — a
100
+ required check that is green because it did nothing is worse than no check. The
101
+ Assure secrets (`ASSURE_URL`, `ASSURE_TOKEN`, `ASSURE_APPLICATION_ID`,
102
+ `ASSURE_ENVIRONMENT_ID`) are separate and are what opens and closes the run.
103
+
104
+ ## Running it
105
+
106
+ ```sh
107
+ pnpm exec playwright install --with-deps chromium # once
108
+ pnpm exec playwright test --list # no credentials needed
109
+ pnpm exec playwright test # needs CORTENA_E2E_*
110
+ pnpm exec playwright test --project chromium-dark-1280
111
+ ```
112
+
113
+ From the extension directory, against the deployed test environment. Nothing
114
+ here starts a server: there is no login page, no token handoff and no consent
115
+ page on a laptop's dev server.
116
+
117
+ ## `locators.ts`
118
+
119
+ Every selector the kit uses is in that one file, keyed on cortena-ui's own
120
+ `data-slot` attributes — load-bearing markup, not test scaffolding somebody can
121
+ delete. A renamed slot breaks every adopter's suite there, once.
122
+
123
+ `SIGN_IN` is the block **DESIGN-119 moves**: it changes how sign-in and the
124
+ signed-out state are mounted, so the shared `AuthScreen`/`LoginScreen` slots are
125
+ tried first and an extension's own labelled form is the fallback. When DESIGN-119
126
+ lands, the fallback goes and nothing else in the kit changes.
@@ -0,0 +1,59 @@
1
+ import { type PlaywrightTestConfig } from '@playwright/test';
2
+ export interface ProtocolConfigOptions {
3
+ /** The deployed test host. An extension's own value, and the only one it must set. */
4
+ host: string;
5
+ /**
6
+ * The package name the unannotated tests roll into as one suite case. It is
7
+ * what tells Assure which extension this run belongs to in the record a
8
+ * person reads; the ids come from `ASSURE_APPLICATION_ID` / `ASSURE_ENVIRONMENT_ID`.
9
+ */
10
+ suitePackage: string;
11
+ /** Defaults to `./e2e`, beside the config. */
12
+ testDir?: string;
13
+ /** Defaults to 1280×900, which is what the project names say. */
14
+ viewport?: {
15
+ width: number;
16
+ height: number;
17
+ };
18
+ /** Per-test timeout. Defaults to 90s: a streamed agent turn is not a fast assertion. */
19
+ timeout?: number;
20
+ /** Environment variable that overrides `host`, for a run against a branch deployment. */
21
+ hostEnvVar?: string;
22
+ /**
23
+ * Merged over the generated config, for the extension that genuinely differs
24
+ * — a longer timeout, an extra `use` option. `retries` and `reporter` are
25
+ * **not** overridable: both are rules (a retry launders a defect into a
26
+ * slower green, and an unreported run is a green pipeline over an empty
27
+ * record), and an option that can be turned off is a rule that will be.
28
+ */
29
+ overrides?: Omit<PlaywrightTestConfig, 'retries' | 'reporter'>;
30
+ }
31
+ /**
32
+ * The Playwright config every extension's layer-3 suite is.
33
+ *
34
+ * An extension copies `playwright.config.ts` and changes two values. What it
35
+ * gets, and why each is not negotiable:
36
+ *
37
+ * **No web server.** The suite runs against the *deployed* test environment,
38
+ * because the login page, the token handoff, the consent page and both themes
39
+ * do not exist until something is deployed (§23.4). Nothing here starts one.
40
+ *
41
+ * **`retries: 0`.** A flaky test is a defect to raise, not a retry to
42
+ * configure around. A retry turns a real defect into a slower green, and the
43
+ * Assure reporter records only the first attempt anyway — so a configured
44
+ * retry buys a pipeline colour and loses the record.
45
+ *
46
+ * **Two projects, light and dark.** §23.4 says both themes, and the project
47
+ * name is how the variant reaches Assure: the reporter parses `{ theme,
48
+ * viewport }` off the name, and a name that encodes neither records no
49
+ * variant rather than a guessed one.
50
+ *
51
+ * **The Assure reporter.** A run is opened per (application, environment) and
52
+ * closed with a verdict, which is what the release gate reads (§23.7, P-35).
53
+ * Without it the suite is a pipeline colour that evaporates.
54
+ *
55
+ * **`fullyParallel: false`.** The specs sign in as one test user in one test
56
+ * org and the agent spec holds a session. Running them against each other is
57
+ * how a suite becomes flaky for a reason that is not a defect.
58
+ */
59
+ export declare function protocolConfig(options: ProtocolConfigOptions): PlaywrightTestConfig;
package/dist/config.js ADDED
@@ -0,0 +1,63 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+ /**
3
+ * The Playwright config every extension's layer-3 suite is.
4
+ *
5
+ * An extension copies `playwright.config.ts` and changes two values. What it
6
+ * gets, and why each is not negotiable:
7
+ *
8
+ * **No web server.** The suite runs against the *deployed* test environment,
9
+ * because the login page, the token handoff, the consent page and both themes
10
+ * do not exist until something is deployed (§23.4). Nothing here starts one.
11
+ *
12
+ * **`retries: 0`.** A flaky test is a defect to raise, not a retry to
13
+ * configure around. A retry turns a real defect into a slower green, and the
14
+ * Assure reporter records only the first attempt anyway — so a configured
15
+ * retry buys a pipeline colour and loses the record.
16
+ *
17
+ * **Two projects, light and dark.** §23.4 says both themes, and the project
18
+ * name is how the variant reaches Assure: the reporter parses `{ theme,
19
+ * viewport }` off the name, and a name that encodes neither records no
20
+ * variant rather than a guessed one.
21
+ *
22
+ * **The Assure reporter.** A run is opened per (application, environment) and
23
+ * closed with a verdict, which is what the release gate reads (§23.7, P-35).
24
+ * Without it the suite is a pipeline colour that evaporates.
25
+ *
26
+ * **`fullyParallel: false`.** The specs sign in as one test user in one test
27
+ * org and the agent spec holds a session. Running them against each other is
28
+ * how a suite becomes flaky for a reason that is not a defect.
29
+ */
30
+ export function protocolConfig(options) {
31
+ const viewport = options.viewport ?? { width: 1280, height: 900 };
32
+ const envVar = options.hostEnvVar ?? 'CORTENA_E2E_BASE_URL';
33
+ const baseURL = process.env[envVar]?.trim() || options.host;
34
+ const project = (name, colorScheme) => ({
35
+ name: `${name}-${viewport.width}`,
36
+ use: { ...devices['Desktop Chrome'], colorScheme, viewport },
37
+ });
38
+ return defineConfig({
39
+ testDir: options.testDir ?? './e2e',
40
+ fullyParallel: false,
41
+ workers: 1,
42
+ timeout: options.timeout ?? 90_000,
43
+ use: {
44
+ baseURL,
45
+ screenshot: 'only-on-failure',
46
+ trace: 'retain-on-failure',
47
+ video: 'off',
48
+ },
49
+ projects: [project('chromium-light', 'light'), project('chromium-dark', 'dark')],
50
+ ...options.overrides,
51
+ // After the spread, not before: these two are the rules, and a spread that
52
+ // could reach them is an option to switch them off.
53
+ retries: 0,
54
+ reporter: [
55
+ ['list'],
56
+ [
57
+ '@ascendenceai/cortena-extensions-assure-reporters/playwright',
58
+ { suitePackage: options.suitePackage },
59
+ ],
60
+ ],
61
+ });
62
+ }
63
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAA6B,MAAM,kBAAkB,CAAC;AA6BpF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,cAAc,CAAC,OAA8B;IAC3D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IAClE,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,IAAI,sBAAsB,CAAC;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAE5D,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,WAA6B,EAAE,EAAE,CAAC,CAAC;QAChE,IAAI,EAAE,GAAG,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE;QACjC,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE;KAC7D,CAAC,CAAC;IAEH,OAAO,YAAY,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,OAAO;QACnC,aAAa,EAAE,KAAK;QACpB,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,MAAM;QAClC,GAAG,EAAE;YACH,OAAO;YACP,UAAU,EAAE,iBAAiB;YAC7B,KAAK,EAAE,mBAAmB;YAC1B,KAAK,EAAE,KAAK;SACb;QACD,QAAQ,EAAE,CAAC,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;QAChF,GAAG,OAAO,CAAC,SAAS;QACpB,2EAA2E;QAC3E,oDAAoD;QACpD,OAAO,EAAE,CAAC;QACV,QAAQ,EAAE;YACR,CAAC,MAAM,CAAC;YACR;gBACE,8DAA8D;gBAC9D,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE;aACvC;SACF;KACF,CAAC,CAAC;AACL,CAAC"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The credentials, read from the environment and from nowhere else.
3
+ *
4
+ * Four variables, the same four `extension-ci.yml`'s `e2e-credentials` job
5
+ * checks for before it lets the end-to-end job run:
6
+ *
7
+ * CORTENA_E2E_ORG_ID the dedicated test org
8
+ * CORTENA_E2E_USER_EMAIL the dedicated test user
9
+ * CORTENA_E2E_USER_PASSWORD its password
10
+ * CORTENA_E2E_TOKEN a pre-minted token, instead of the two above
11
+ *
12
+ * Never a real user and never a shared account (§23.4). Nothing here has a
13
+ * default and nothing here is committed: a fixture that falls back to a
14
+ * checked-in credential is a credential in the repository, and the whole point
15
+ * of the CI job being *skipped* without them is that a suite which cannot sign
16
+ * in must not report a pass.
17
+ */
18
+ export interface E2ECredentials {
19
+ orgId: string;
20
+ email?: string;
21
+ password?: string;
22
+ /** A launch token, which is also how CortenaWeb hands the app a session. */
23
+ token?: string;
24
+ }
25
+ export declare class MissingCredentialsError extends Error {
26
+ readonly name = "MissingCredentialsError";
27
+ }
28
+ /**
29
+ * Reads the four. Throws rather than returning a half-filled object: a suite
30
+ * that starts with no password fails on the first assertion of the first spec,
31
+ * and the failure it reports is "the heading was not visible" rather than "you
32
+ * did not set a password".
33
+ */
34
+ export declare function readCredentials(env?: NodeJS.ProcessEnv): E2ECredentials;
35
+ /** Whether a run could sign in at all, without throwing — for `test.skip` reasons. */
36
+ export declare function hasCredentials(env?: NodeJS.ProcessEnv): boolean;
37
+ /**
38
+ * The environment under test, for the record Assure keeps. A run is opened per
39
+ * (application, environment) and `CORTENA_E2E_ENVIRONMENT` is what names the
40
+ * second half in a human's words; the ids the reporter posts are Assure's own
41
+ * (`ASSURE_ENVIRONMENT_ID`).
42
+ */
43
+ export declare function environmentName(env?: NodeJS.ProcessEnv): string;
package/dist/env.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The credentials, read from the environment and from nowhere else.
3
+ *
4
+ * Four variables, the same four `extension-ci.yml`'s `e2e-credentials` job
5
+ * checks for before it lets the end-to-end job run:
6
+ *
7
+ * CORTENA_E2E_ORG_ID the dedicated test org
8
+ * CORTENA_E2E_USER_EMAIL the dedicated test user
9
+ * CORTENA_E2E_USER_PASSWORD its password
10
+ * CORTENA_E2E_TOKEN a pre-minted token, instead of the two above
11
+ *
12
+ * Never a real user and never a shared account (§23.4). Nothing here has a
13
+ * default and nothing here is committed: a fixture that falls back to a
14
+ * checked-in credential is a credential in the repository, and the whole point
15
+ * of the CI job being *skipped* without them is that a suite which cannot sign
16
+ * in must not report a pass.
17
+ */
18
+ export class MissingCredentialsError extends Error {
19
+ name = 'MissingCredentialsError';
20
+ }
21
+ const MESSAGE = [
22
+ 'The end-to-end kit needs a user to sign in as.',
23
+ '',
24
+ ' CORTENA_E2E_ORG_ID required — the dedicated test org',
25
+ ' CORTENA_E2E_TOKEN a pre-minted token, or',
26
+ ' CORTENA_E2E_USER_EMAIL the test user and',
27
+ ' CORTENA_E2E_USER_PASSWORD its password',
28
+ '',
29
+ 'In CI these are repository secrets and the end-to-end job is skipped when they',
30
+ 'are absent — a skipped job is visibly not a pass. On a laptop, export them for',
31
+ 'the test org and run `pnpm exec playwright test` from the extension directory.',
32
+ ].join('\n');
33
+ /**
34
+ * Reads the four. Throws rather than returning a half-filled object: a suite
35
+ * that starts with no password fails on the first assertion of the first spec,
36
+ * and the failure it reports is "the heading was not visible" rather than "you
37
+ * did not set a password".
38
+ */
39
+ export function readCredentials(env = process.env) {
40
+ const orgId = env.CORTENA_E2E_ORG_ID?.trim();
41
+ const token = env.CORTENA_E2E_TOKEN?.trim();
42
+ const email = env.CORTENA_E2E_USER_EMAIL?.trim();
43
+ const password = env.CORTENA_E2E_USER_PASSWORD?.trim();
44
+ if (!orgId || !(token || (email && password)))
45
+ throw new MissingCredentialsError(MESSAGE);
46
+ return { orgId, email, password, token };
47
+ }
48
+ /** Whether a run could sign in at all, without throwing — for `test.skip` reasons. */
49
+ export function hasCredentials(env = process.env) {
50
+ try {
51
+ readCredentials(env);
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ /**
59
+ * The environment under test, for the record Assure keeps. A run is opened per
60
+ * (application, environment) and `CORTENA_E2E_ENVIRONMENT` is what names the
61
+ * second half in a human's words; the ids the reporter posts are Assure's own
62
+ * (`ASSURE_ENVIRONMENT_ID`).
63
+ */
64
+ export function environmentName(env = process.env) {
65
+ return env.CORTENA_E2E_ENVIRONMENT?.trim() || 'cortena-test';
66
+ }
67
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAUH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAC9B,IAAI,GAAG,yBAAyB,CAAC;CACpD;AAED,MAAM,OAAO,GAAG;IACd,gDAAgD;IAChD,EAAE;IACF,gEAAgE;IAChE,qDAAqD;IACrD,gDAAgD;IAChD,2CAA2C;IAC3C,EAAE;IACF,gFAAgF;IAChF,gFAAgF;IAChF,gFAAgF;CACjF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAyB,OAAO,CAAC,GAAG;IAClE,MAAM,KAAK,GAAG,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,CAAC;IAC7C,MAAM,KAAK,GAAG,GAAG,CAAC,iBAAiB,EAAE,IAAI,EAAE,CAAC;IAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE,CAAC;IACjD,MAAM,QAAQ,GAAG,GAAG,CAAC,yBAAyB,EAAE,IAAI,EAAE,CAAC;IAEvD,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC,CAAC;QAAE,MAAM,IAAI,uBAAuB,CAAC,OAAO,CAAC,CAAC;IAC1F,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC3C,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,cAAc,CAAC,MAAyB,OAAO,CAAC,GAAG;IACjE,IAAI,CAAC;QACH,eAAe,CAAC,GAAG,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAyB,OAAO,CAAC,GAAG;IAClE,OAAO,GAAG,CAAC,uBAAuB,EAAE,IAAI,EAAE,IAAI,cAAc,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,107 @@
1
+ import { type Page } from '@playwright/test';
2
+ export interface ConsentOptions {
3
+ /**
4
+ * The extension's own origin. The resource, the authorisation server and the
5
+ * scopes are **discovered** from it, the way an MCP client discovers them —
6
+ * so a suite that hard-codes them passes against a deployment whose metadata
7
+ * says something else entirely.
8
+ */
9
+ host: string;
10
+ /**
11
+ * A pre-registered MCP client. Absent, one is registered dynamically at the
12
+ * authorisation server's `registration_endpoint`, which is what an MCP
13
+ * client with no prior relationship does.
14
+ */
15
+ clientId?: string;
16
+ /** Which scopes to ask for. Defaults to the first the resource advertises. */
17
+ scopes?: string[];
18
+ /** Where the authorisation code would be delivered. Never followed. */
19
+ redirectUri?: string;
20
+ }
21
+ export interface ProtectedResourceMetadata {
22
+ resource: string;
23
+ authorization_servers: string[];
24
+ scopes_supported?: string[];
25
+ }
26
+ /** What a dynamic registration hands back, and everything needed to undo it. */
27
+ export interface ClientRegistration {
28
+ clientId: string;
29
+ /** RFC 7592's credential for managing this client. Issued once, never re-issued. */
30
+ registrationAccessToken?: string;
31
+ registrationClientUri?: string;
32
+ }
33
+ export interface AuthorizationServerMetadata {
34
+ authorization_endpoint: string;
35
+ registration_endpoint?: string;
36
+ code_challenge_methods_supported?: string[];
37
+ }
38
+ /**
39
+ * The MCP consent page (§23.4, rules P-08 and P-16).
40
+ *
41
+ * This is the one screen of the six that **no extension renders**. The OAuth
42
+ * flow lands on cortena-auth's own hosted page — a different deployment, a
43
+ * different repository, and today not built from cortena-ui at all. So it is
44
+ * located by what a consent page *is* rather than by a `data-slot` this
45
+ * repository could promise: a page that names the client and the resource,
46
+ * lists the scopes being asked for, and offers a way to allow and a way to
47
+ * refuse. `ConsentScreen`'s slots are tried first, so the day cortena-auth
48
+ * adopts the shared composition this stops being heuristic and nothing here
49
+ * changes.
50
+ *
51
+ * The whole request is **discovered**, not written down: RFC 9728
52
+ * `/.well-known/oauth-protected-resource` on the extension's own host gives the
53
+ * resource identifier, the authorisation server and the scopes; the server's
54
+ * metadata gives the endpoints. That is exactly the sequence an MCP client
55
+ * performs, so a deployment whose metadata is wrong fails here rather than
56
+ * passing against values a test file remembered.
57
+ *
58
+ * It is asserted **without accepting**. Pressing Allow mints a real grant for
59
+ * the test user against a real resource, and a suite that does that on every
60
+ * run leaves a trail of grants nobody revokes. What the protocol requires is
61
+ * that the page exists, says what is being asked for, and can be refused.
62
+ */
63
+ export declare function consent(page: Page, options: ConsentOptions): Promise<void>;
64
+ /** RFC 9728, from the extension's own host. */
65
+ export declare function discoverProtectedResource(page: Page, host: string): Promise<ProtectedResourceMetadata>;
66
+ export declare function discoverAuthorizationServer(page: Page, issuer: string): Promise<AuthorizationServerMetadata>;
67
+ /**
68
+ * Dynamic client registration — RFC 7591, and what every MCP client that has
69
+ * never met this server does on first use. A suite that needs a client id
70
+ * provisioned by hand is a suite that stops working the day the environment is
71
+ * rebuilt, which is the day it is most needed.
72
+ */
73
+ export declare function registerClient(page: Page, server: AuthorizationServerMetadata, redirectUri: string): Promise<ClientRegistration>;
74
+ /**
75
+ * Deletes the client this run registered — RFC 7592.
76
+ *
77
+ * Best effort, and deliberately so. Client management is optional: a server
78
+ * that does not implement it answers 404, 405 or 501, and failing the consent
79
+ * spec because the *cleanup* was not supported would report the product broken
80
+ * over a housekeeping detail. It says what happened and moves on; what it must
81
+ * never do is silently skip cleanup on a server that does support it, which is
82
+ * why the outcome is always logged.
83
+ */
84
+ export declare function deleteClient(page: Page, registration: ClientRegistration): Promise<void>;
85
+ /**
86
+ * The authorisation request, with PKCE.
87
+ *
88
+ * The verifier is fixed and public on purpose: no code is ever exchanged — the
89
+ * suite refuses the grant — so there is nothing for a secret to protect, and a
90
+ * generated one would only make the failure harder to reproduce.
91
+ */
92
+ export declare const PKCE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
93
+ export declare const PKCE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
94
+ export declare function authorizeUrl(input: {
95
+ server: AuthorizationServerMetadata;
96
+ clientId: string;
97
+ redirectUri: string;
98
+ resource: string;
99
+ scopes: string[];
100
+ }): string;
101
+ /**
102
+ * cortena-auth's own sign-in, which the authorisation request lands on before
103
+ * it will show a consent page. Its markup is the server's, not cortena-ui's,
104
+ * so this is by accessible name and nothing else. Already signed in, there is
105
+ * no form and this returns.
106
+ */
107
+ export declare function signInAtAuthorizationServer(page: Page): Promise<void>;