@terpjs/conformance 0.5.5 → 0.5.7

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,43 @@
1
+ import { type Page } from "@playwright/test";
2
+ /**
3
+ * The administrator the base-profile flows sign in as. The default matches the bundled example
4
+ * workbench's seed; a generated repo points these at its own seeded admin via
5
+ * `TERP_E2E_ADMIN_EMAIL` / `TERP_E2E_ADMIN_PASSWORD` (or passes credentials to `login`)
6
+ * without editing the suite.
7
+ */
8
+ export declare const ADMIN: {
9
+ email: string;
10
+ password: string;
11
+ };
12
+ /**
13
+ * A writer (rank at/above the write threshold) and a read-only user, for an app's RBAC flows that
14
+ * assert write-gated controls appear for a writer and hide for a viewer. The defaults match the
15
+ * bundled example workbench's seed and are overridable per app via the matching `TERP_E2E_*`
16
+ * variables.
17
+ */
18
+ export declare const EDITOR: {
19
+ email: string;
20
+ password: string;
21
+ };
22
+ export declare const VIEWER: {
23
+ email: string;
24
+ password: string;
25
+ };
26
+ /**
27
+ * Sign in through the real login screen. App-agnostic: the login screen and session are
28
+ * base-profile (identical in every Terp app), so this is the reusable entry point any app's
29
+ * conformance suite composes. Success is the sign-in screen being replaced by the app shell;
30
+ * callers assert their own landing content afterwards.
31
+ */
32
+ export declare function login(page: Page, credentials?: {
33
+ email: string;
34
+ password: string;
35
+ }): Promise<void>;
36
+ /**
37
+ * Sign out through the shell's account menu and assert the session is gone. Base-profile: the
38
+ * user menu (avatar at the bottom of every Terp app's sidebar) opens Settings and Sign out;
39
+ * sign-out revokes the token server-side (ADR 0031), so this is reusable across apps. Success
40
+ * is the app shell being replaced by the sign-in screen.
41
+ */
42
+ export declare function logout(page: Page): Promise<void>;
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,KAAK;;;CAGjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,MAAM;;;CAGlB,CAAC;AAEF,eAAO,MAAM,MAAM;;;CAGlB,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,KAAK,CACzB,IAAI,EAAE,IAAI,EACV,WAAW,GAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAU,GACvD,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;;;GAKG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAItD"}
@@ -1,5 +1,4 @@
1
- import { expect, type Page } from "@playwright/test";
2
-
1
+ import { expect } from "@playwright/test";
3
2
  /**
4
3
  * The administrator the base-profile flows sign in as. The default matches the bundled example
5
4
  * workbench's seed; a generated repo points these at its own seeded admin via
@@ -7,10 +6,9 @@ import { expect, type Page } from "@playwright/test";
7
6
  * without editing the suite.
8
7
  */
9
8
  export const ADMIN = {
10
- email: process.env.TERP_E2E_ADMIN_EMAIL ?? "admin@acme.test",
11
- password: process.env.TERP_E2E_ADMIN_PASSWORD ?? "correct horse battery staple",
9
+ email: process.env.TERP_E2E_ADMIN_EMAIL ?? "admin@acme.test",
10
+ password: process.env.TERP_E2E_ADMIN_PASSWORD ?? "correct horse battery staple",
12
11
  };
13
-
14
12
  /**
15
13
  * A writer (rank at/above the write threshold) and a read-only user, for an app's RBAC flows that
16
14
  * assert write-gated controls appear for a writer and hide for a viewer. The defaults match the
@@ -18,43 +16,38 @@ export const ADMIN = {
18
16
  * variables.
19
17
  */
20
18
  export const EDITOR = {
21
- email: process.env.TERP_E2E_EDITOR_EMAIL ?? "editor@acme.test",
22
- password: process.env.TERP_E2E_EDITOR_PASSWORD ?? "correct horse battery staple",
19
+ email: process.env.TERP_E2E_EDITOR_EMAIL ?? "editor@acme.test",
20
+ password: process.env.TERP_E2E_EDITOR_PASSWORD ?? "correct horse battery staple",
23
21
  };
24
-
25
22
  export const VIEWER = {
26
- email: process.env.TERP_E2E_VIEWER_EMAIL ?? "viewer@acme.test",
27
- password: process.env.TERP_E2E_VIEWER_PASSWORD ?? "correct horse battery staple",
23
+ email: process.env.TERP_E2E_VIEWER_EMAIL ?? "viewer@acme.test",
24
+ password: process.env.TERP_E2E_VIEWER_PASSWORD ?? "correct horse battery staple",
28
25
  };
29
-
30
26
  /**
31
27
  * Sign in through the real login screen. App-agnostic: the login screen and session are
32
28
  * base-profile (identical in every Terp app), so this is the reusable entry point any app's
33
29
  * conformance suite composes. Success is the sign-in screen being replaced by the app shell;
34
30
  * callers assert their own landing content afterwards.
35
31
  */
36
- export async function login(
37
- page: Page,
38
- credentials: { email: string; password: string } = ADMIN,
39
- ): Promise<void> {
40
- await page.goto("/");
41
- await expect(page.getByRole("heading", { name: "Sign in" })).toBeVisible();
42
- await page.getByPlaceholder("Email").fill(credentials.email);
43
- await page.getByPlaceholder("Password").fill(credentials.password);
44
- await page.getByRole("button", { name: "Sign in" }).click();
45
- await expect(page.getByRole("heading", { name: "Sign in" })).toHaveCount(0, {
46
- timeout: 15_000,
47
- });
32
+ export async function login(page, credentials = ADMIN) {
33
+ await page.goto("/");
34
+ await expect(page.getByRole("heading", { name: "Sign in" })).toBeVisible();
35
+ await page.getByPlaceholder("Email").fill(credentials.email);
36
+ await page.getByPlaceholder("Password").fill(credentials.password);
37
+ await page.getByRole("button", { name: "Sign in" }).click();
38
+ await expect(page.getByRole("heading", { name: "Sign in" })).toHaveCount(0, {
39
+ timeout: 15_000,
40
+ });
48
41
  }
49
-
50
42
  /**
51
43
  * Sign out through the shell's account menu and assert the session is gone. Base-profile: the
52
44
  * user menu (avatar at the bottom of every Terp app's sidebar) opens Settings and Sign out;
53
45
  * sign-out revokes the token server-side (ADR 0031), so this is reusable across apps. Success
54
46
  * is the app shell being replaced by the sign-in screen.
55
47
  */
56
- export async function logout(page: Page): Promise<void> {
57
- await page.getByRole("button", { name: "Account menu" }).click();
58
- await page.getByRole("menuitem", { name: "Sign out" }).click();
59
- await expect(page.getByRole("heading", { name: "Sign in" })).toBeVisible();
48
+ export async function logout(page) {
49
+ await page.getByRole("button", { name: "Account menu" }).click();
50
+ await page.getByRole("menuitem", { name: "Sign out" }).click();
51
+ await expect(page.getByRole("heading", { name: "Sign in" })).toBeVisible();
60
52
  }
53
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAa,MAAM,kBAAkB,CAAC;AAErD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,iBAAiB;IAC5D,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,8BAA8B;CAChF,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,kBAAkB;IAC9D,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,8BAA8B;CACjF,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,kBAAkB;IAC9D,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,8BAA8B;CACjF,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,IAAU,EACV,cAAmD,KAAK;IAExD,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC3E,MAAM,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC7D,MAAM,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IACnE,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IAC5D,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE;QAC1E,OAAO,EAAE,MAAM;KAChB,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,IAAU;IACrC,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IACjE,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IAC/D,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;AAC7E,CAAC"}
package/package.json CHANGED
@@ -1,20 +1,30 @@
1
1
  {
2
2
  "name": "@terpjs/conformance",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "description": "Terp conformance \u2014 app-agnostic Playwright end-to-end flows over a running Terp stack (base profile: auth gating + session lifecycle) plus the login/role helpers an app's own e2e suite composes.",
6
- "main": "./src/index.ts",
7
- "types": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
8
  "exports": {
9
- ".": "./src/index.ts"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
10
13
  },
14
+ "files": [
15
+ "dist"
16
+ ],
11
17
  "scripts": {
18
+ "build": "tsc -p tsconfig.build.json",
19
+ "prepack": "npm run build",
20
+ "typecheck": "tsc --noEmit",
12
21
  "test": "playwright test",
13
22
  "report": "playwright show-report"
14
23
  },
15
24
  "devDependencies": {
16
25
  "@playwright/test": "^1.50.0",
17
- "@types/node": "^22.10.0"
26
+ "@types/node": "^22.10.0",
27
+ "typescript": "^5.9.3"
18
28
  },
19
29
  "license": "Apache-2.0",
20
30
  "repository": {
@@ -1,19 +0,0 @@
1
- import { defineConfig, devices } from "@playwright/test";
2
-
3
- // The suite runs against an ALREADY-RUNNING Terp stack — the Docker workbench
4
- // (`docker compose -f apps/example/docker-compose.yml up`) or `terp dev`. CI brings the stack
5
- // up first; locally, point TERP_E2E_BASE_URL at your running frontend (e.g. the workbench port).
6
- const baseURL = process.env.TERP_E2E_BASE_URL ?? "http://localhost:5173";
7
-
8
- export default defineConfig({
9
- testDir: "./tests",
10
- fullyParallel: true,
11
- forbidOnly: !!process.env.CI,
12
- retries: process.env.CI ? 2 : 0,
13
- reporter: process.env.CI ? [["list"], ["html", { open: "never" }]] : "list",
14
- use: {
15
- baseURL,
16
- trace: "on-first-retry",
17
- },
18
- projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
19
- });
@@ -1,50 +0,0 @@
1
- import { expect, test } from "@playwright/test";
2
-
3
- import { ADMIN, login, logout } from "../src/index";
4
-
5
- // Base-profile auth — reusable across ANY Terp app (the login screen + session are identical
6
- // everywhere): a signed-out visitor is gated to the login screen, the seeded admin can sign in
7
- // and reach the app shell, sign out again, and bad credentials are refused — all over the real
8
- // deny-by-default backend, not a mock.
9
-
10
- test("an unauthenticated visitor is gated to the sign-in screen", async ({ page }) => {
11
- await page.goto("/");
12
- await expect(page.getByRole("heading", { name: "Sign in" })).toBeVisible();
13
- // The authenticated app shell (its primary navigation) is not reachable without a session.
14
- await expect(page.getByRole("navigation", { name: "Primary" })).toHaveCount(0);
15
- });
16
-
17
- test("the seeded admin can sign in and reach the app shell", async ({ page }) => {
18
- await login(page, ADMIN);
19
- await expect(page.getByRole("navigation", { name: "Primary" })).toBeVisible();
20
- });
21
-
22
- test("a signed-in session survives a page reload", async ({ page }) => {
23
- await login(page, ADMIN);
24
- await expect(page.getByRole("navigation", { name: "Primary" })).toBeVisible();
25
-
26
- await page.reload();
27
-
28
- // ADR 0054: the access token is still memory-only, but the httpOnly refresh cookie restores
29
- // a fresh access token on boot, so a normal reload keeps the user in the app shell.
30
- await expect(page.getByRole("navigation", { name: "Primary" })).toBeVisible();
31
- await expect(page.getByRole("heading", { name: "Sign in" })).toHaveCount(0);
32
- });
33
-
34
- test("a signed-in user can sign out and is returned to the sign-in screen", async ({ page }) => {
35
- await login(page, ADMIN);
36
- await expect(page.getByRole("navigation", { name: "Primary" })).toBeVisible();
37
- await logout(page);
38
- // The session is gone: the app shell is no longer reachable, only the login screen.
39
- await expect(page.getByRole("navigation", { name: "Primary" })).toHaveCount(0);
40
- });
41
-
42
- test("bad credentials are refused", async ({ page }) => {
43
- await page.goto("/");
44
- // A non-existent account, so a failed attempt never locks out the seeded admin.
45
- await page.getByPlaceholder("Email").fill("nobody@acme.test");
46
- await page.getByPlaceholder("Password").fill("definitely-not-the-password");
47
- await page.getByRole("button", { name: "Sign in" }).click();
48
- await expect(page.getByText("Sign-in failed")).toBeVisible();
49
- await expect(page.getByRole("navigation", { name: "Primary" })).toHaveCount(0);
50
- });
@@ -1,85 +0,0 @@
1
- import { expect, test, type APIRequestContext } from "@playwright/test";
2
-
3
- import { ADMIN } from "../src/index";
4
-
5
- // Terp Standard black-box probes (spec/catalog entries with layer "black-box"): these
6
- // invariants are observable on a RUNNING app with no source access, so they are portable to
7
- // any stack — this file is the reference realisation the catalog's `black-box` enforcement
8
- // entries point at. Like auth.spec.ts, the probes use only the base profile (auth + the
9
- // seeded admin) plus the `users` capability every generated app starts with, so they stay
10
- // app-agnostic. Each test title is the catalog entry's enforcement `ref`.
11
-
12
- async function bearer(request: APIRequestContext): Promise<{ Authorization: string }> {
13
- const response = await request.post("/api/v1/auth/login", { data: ADMIN });
14
- expect(response.ok()).toBe(true);
15
- const { access_token } = (await response.json()) as { access_token: string };
16
- return { Authorization: "Bearer " + access_token };
17
- }
18
-
19
- // spec/catalog/backend/list_routes_paginate.json
20
- test("standard: list routes return a capped Page envelope", async ({ request }) => {
21
- const headers = await bearer(request);
22
-
23
- // A collection GET is a uniform Page envelope, never a bare JSON array.
24
- const page = await request.get("/api/v1/users/", { headers });
25
- expect(page.ok()).toBe(true);
26
- const body = (await page.json()) as Record<string, unknown>;
27
- expect(Array.isArray(body)).toBe(false);
28
- expect(body).toMatchObject({ items: expect.any(Array), total: expect.any(Number) });
29
- expect(typeof body.limit).toBe("number");
30
- expect(typeof body.skip).toBe("number");
31
-
32
- // The page size is capped fail-closed: an oversized limit is refused, not honoured.
33
- const oversized = await request.get("/api/v1/users/?limit=100000", { headers });
34
- expect(oversized.status()).toBe(422);
35
- });
36
-
37
- // spec/catalog/backend/safe_methods_are_read_only.json
38
- test("standard: safe methods observably mutate nothing", async ({ request }) => {
39
- const headers = await bearer(request);
40
-
41
- // Reading a collection is repeatable: the GETs themselves change no state, so the
42
- // second read reports exactly the population the first one saw.
43
- const first = await request.get("/api/v1/users/", { headers });
44
- expect(first.ok()).toBe(true);
45
- const before = (await first.json()) as { total: number; items: unknown[] };
46
-
47
- const second = await request.get("/api/v1/users/", { headers });
48
- expect(second.ok()).toBe(true);
49
- const after = (await second.json()) as { total: number; items: unknown[] };
50
-
51
- expect(after.total).toBe(before.total);
52
- expect(after.items.length).toBe(before.items.length);
53
- });
54
-
55
- // spec/catalog/backend/schemas_exclude_sensitive_fields.json
56
- const SENSITIVE_KEY = new RegExp(
57
- "(?:^|_)(?:password|passwd|pwd|passphrase|secret|salt|api_key|apikey" +
58
- "|private_key|privatekey|credentials?)(?:$|_)|(?:^|_)token$",
59
- );
60
- const SENSITIVE_KEY_EXCLUSIONS = new Set(["token_version", "version"]);
61
-
62
- function sensitiveKeys(value: unknown, found: string[] = []): string[] {
63
- if (Array.isArray(value)) {
64
- value.forEach((item) => sensitiveKeys(item, found));
65
- } else if (value !== null && typeof value === "object") {
66
- for (const [key, nested] of Object.entries(value)) {
67
- if (!SENSITIVE_KEY_EXCLUSIONS.has(key.toLowerCase()) && SENSITIVE_KEY.test(key.toLowerCase())) {
68
- found.push(key);
69
- }
70
- sensitiveKeys(nested, found);
71
- }
72
- }
73
- return found;
74
- }
75
-
76
- test("standard: responses never expose credential-shaped fields", async ({ request }) => {
77
- const headers = await bearer(request);
78
- for (const path of ["/api/v1/users/", "/api/v1/me/"]) {
79
- const response = await request.get(path, { headers });
80
- expect(response.ok()).toBe(true);
81
- expect(sensitiveKeys(await response.json()), `${path} leaked a credential-shaped field`).toEqual(
82
- [],
83
- );
84
- }
85
- });
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "types": ["node"],
7
- "strict": true,
8
- "noEmit": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true
12
- },
13
- "include": ["src", "tests", "playwright.config.ts"]
14
- }