@appliqation/automation-sdk 2.5.0 → 2.7.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.
package/README.md CHANGED
@@ -274,6 +274,141 @@ npx playwright test tests/login.spec.js -- --appq --appq_run_title="Login Tests
274
274
 
275
275
  ---
276
276
 
277
+ ## Authenticated apps (gated SUTs)
278
+
279
+ If your application requires login, the SDK ships a portable auth setup
280
+ that works the same way in Appliqation's executor and in your own CI/local.
281
+
282
+ The login flow itself is **your code, in your repo** (`tests/appliqation/auth/login.ts`).
283
+ Appliqation's executor and `npx appq-auth-setup` both run that same function — so
284
+ SSO, MFA, multi-step login, OAuth, captcha bypass, custom IDP redirects all work
285
+ because you write the Playwright code that handles them.
286
+
287
+ ### One-time setup per project
288
+
289
+ 1. **In Appliqation**: configure roles + credentials in **Project Settings → Auth Config** (role-by-role usernames/passwords). Credentials are encrypted server-side via AWS Secrets Manager.
290
+
291
+ 2. **In your repo**: create `tests/appliqation/auth/login.ts` describing your login flow:
292
+
293
+ ```typescript
294
+ import { defineLogin } from '@appliqation/automation-sdk/login';
295
+
296
+ export default defineLogin(async (page, { username, password, role, baseURL }) => {
297
+ await page.goto('/login'); // baseURL handles env (staging/prod)
298
+ await page.getByLabel('Email').fill(username);
299
+ await page.getByLabel('Password').fill(password);
300
+ await page.getByRole('button', { name: 'Sign in' }).click();
301
+ await page.waitForURL('**/dashboard');
302
+
303
+ // Branch on role for multi-role flows / SSO / role-specific landing pages
304
+ if (role === 'admin') {
305
+ await page.getByRole('button', { name: 'Switch to admin view' }).click();
306
+ }
307
+ });
308
+ ```
309
+
310
+ Push to the main branch of your project's GitHub repo. Appliqation's GitHub webhook ingests the file automatically.
311
+
312
+ 3. **In Appliqation**: click the small **📥 download .env for CI** link on the project settings page. Fill in the placeholders and add to your CI secrets:
313
+
314
+ ```bash
315
+ APPLIQATION_API_KEY="<paste your project's API key>"
316
+ APPLIQATION_BASE_URL="https://appliqation.io"
317
+ APPLIQATION_PROJECT_KEY=126
318
+ APPLIQATION_SUT_BASE_URL="<your SUT URL, e.g. https://staging.acme.com>"
319
+
320
+ # Role: default
321
+ APPQ_PROJECT_126_DEFAULT_USERNAME="<paste username>"
322
+ APPQ_PROJECT_126_DEFAULT_PASSWORD="<paste password>"
323
+ ```
324
+
325
+ `APPLIQATION_SUT_BASE_URL` is the URL of the system you're testing — your login function's `await page.goto('/login')` resolves against this, so the same code works against staging, preprod, and prod by changing one env var.
326
+
327
+ 4. **(Optional)** Click **🧪 Test login** on the project settings page to validate your `login.ts` works against the live SUT. Surfaces success / specific failure inline within ~10 seconds.
328
+
329
+ ### In your test files
330
+
331
+ Use `setupAuth` to declare which storage state Playwright should use:
332
+
333
+ ```typescript
334
+ const { mapAppqUuid, setupAuth } = require('@appliqation/automation-sdk/utils');
335
+ const { test, expect } = require('@playwright/test');
336
+
337
+ test.use({
338
+ storageState: setupAuth({ project_id: 126, role: 'default' }),
339
+ });
340
+
341
+ test('manager dashboard loads', async ({ page }, testInfo) => {
342
+ mapAppqUuid(testInfo, '1141-...');
343
+ await page.goto('/dashboard'); // already authenticated
344
+ });
345
+ ```
346
+
347
+ `setupAuth()` returns a deterministic file path (e.g. `~/.appq-auth/project-126-default.json`). It does NOT perform login — that's handled separately:
348
+
349
+ - **In Appliqation's executor (cloud runs)**: the LoginHelper imports your `login.ts` from the canonical store, runs it, and writes the storage state at this path before your test runs. Nothing else for you to do.
350
+ - **In your CI / local**: run `npx appq-auth-setup` once before `playwright test`.
351
+
352
+ ### CI workflow
353
+
354
+ ```yaml
355
+ # .github/workflows/playwright.yml
356
+ - run: npx appq-auth-setup --project-id 126 --role default
357
+ - run: npx playwright test
358
+ ```
359
+
360
+ The CLI:
361
+ 1. Reads `APPQ_PROJECT_126_DEFAULT_USERNAME` / `_PASSWORD` from env
362
+ 2. Reads `APPLIQATION_SUT_BASE_URL` for the SUT base URL
363
+ 3. Dynamically imports `tests/appliqation/auth/login.ts` from your local checkout
364
+ 4. Launches Chromium, runs your login function, saves the storage state to the path `setupAuth()` will resolve to
365
+ 5. Tests run, already authenticated
366
+
367
+ Re-run the CLI when sessions expire (typical: once per CI run). Idempotent and fast on cache hits.
368
+
369
+ #### TypeScript loader (peer dependency)
370
+
371
+ If your `login.ts` is TypeScript (recommended for the type checking on `LoginContext`), the CLI needs a TS loader installed in your project. Install one as a dev dep:
372
+
373
+ ```bash
374
+ npm install --save-dev tsx
375
+ # or
376
+ npm install --save-dev ts-node
377
+ ```
378
+
379
+ Plain `.js` / `.mjs` / `.cjs` login files don't need either.
380
+
381
+ #### Custom login file path
382
+
383
+ Default convention is `tests/appliqation/auth/login.ts`. Override with the `--login-file` flag or `APPQ_LOGIN_FILE` env var if your project uses a different layout:
384
+
385
+ ```yaml
386
+ - run: npx appq-auth-setup --project-id 126 --role default --login-file e2e/auth/login.ts
387
+ ```
388
+
389
+ ### Multiple roles
390
+
391
+ Configure each role in Project Settings, download the updated `.env` template (it lists all configured roles), fill in the values:
392
+
393
+ ```bash
394
+ APPQ_PROJECT_126_DEFAULT_USERNAME="..."
395
+ APPQ_PROJECT_126_DEFAULT_PASSWORD="..."
396
+ APPQ_PROJECT_126_MANAGER_USERNAME="..."
397
+ APPQ_PROJECT_126_MANAGER_PASSWORD="..."
398
+ ```
399
+
400
+ Then in CI — one `appq-auth-setup` call per role you want to test:
401
+
402
+ ```yaml
403
+ - run: npx appq-auth-setup --project-id 126 --role default
404
+ - run: npx appq-auth-setup --project-id 126 --role manager
405
+ - run: npx playwright test
406
+ ```
407
+
408
+ Tests reference the role they need: `setupAuth({ project_id: 126, role: 'manager' })`. Your `login.ts` receives the role in its `LoginContext` so you can branch login flow per role if needed.
409
+
410
+ ---
411
+
277
412
  ## Enabling Appliqation Reporting (--appq Flag)
278
413
 
279
414
  By default, Appliqation reporting is **DISABLED**. You must explicitly enable it by adding the `--appq` flag to your test command.
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@appliqation/automation-sdk",
3
- "version": "2.5.0",
4
- "description": "Appliqation Automation SDK with API key authentication, custom run titles, and framework-specific reporters",
3
+ "version": "2.7.0",
4
+ "description": "Appliqation Automation SDK API key auth, custom run titles, framework reporters, portable storageState (setupAuth), and customer-defined login flows (defineLogin) for gated apps",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
+ "bin": {
8
+ "appq-auth-setup": "./src/cli/auth-setup.js"
9
+ },
7
10
  "exports": {
8
11
  ".": {
9
12
  "require": "./src/index.js",
@@ -40,6 +43,11 @@
40
43
  "./utils": {
41
44
  "require": "./src/utils/index.js",
42
45
  "import": "./src/utils/index.js"
46
+ },
47
+ "./login": {
48
+ "types": "./src/login/index.d.ts",
49
+ "require": "./src/login/index.js",
50
+ "import": "./src/login/index.js"
43
51
  }
44
52
  },
45
53
  "files": [
@@ -110,7 +118,7 @@
110
118
  }
111
119
  },
112
120
  "engines": {
113
- "node": ">=16.0.0"
121
+ "node": ">=18.0.0"
114
122
  },
115
123
  "repository": {
116
124
  "type": "git",
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * appq-auth-setup — populate Playwright storageState for an Appliqation
4
+ * project's role by dynamic-importing the customer's login.ts file
5
+ * and running it with role-specific creds from env vars.
6
+ *
7
+ * npx appq-auth-setup --project-id 126 --role manager
8
+ *
9
+ * Customer setup (one-time, per project):
10
+ * 1. Configure roles in Appliqation Project Settings → Auth Config
11
+ * 2. Write `tests/appliqation/auth/login.ts` in their repo:
12
+ *
13
+ * import { defineLogin } from '@appliqation/automation-sdk/login';
14
+ * export default defineLogin(async (page, { username, password }) => {
15
+ * await page.goto('/login');
16
+ * await page.getByLabel('Email').fill(username);
17
+ * await page.getByLabel('Password').fill(password);
18
+ * await page.getByRole('button', { name: 'Sign in' }).click();
19
+ * await page.waitForURL('**\/dashboard');
20
+ * });
21
+ *
22
+ * 3. Download the .env template from Project Settings, fill in
23
+ * values, drop into CI secrets
24
+ * 4. Add to CI yaml (one line per role used by their tests):
25
+ * - run: npx appq-auth-setup --project-id 126 --role default
26
+ * - run: npx playwright test
27
+ *
28
+ * What this CLI does:
29
+ * 1. Read APPQ_PROJECT_<id>_<ROLE>_USERNAME / _PASSWORD from env
30
+ * 2. Dynamic-import the customer's login.ts (path discoverable via
31
+ * --login-file flag, env var, or convention)
32
+ * 3. Launch Chromium, run customer's login function with creds +
33
+ * baseURL from APPLIQATION_SUT_BASE_URL env var
34
+ * 4. Save resulting Playwright storageState to the canonical path
35
+ * from setupAuth({ project_id, role })
36
+ *
37
+ * Tests then load via:
38
+ * test.use({ storageState: setupAuth({ project_id, role }) })
39
+ *
40
+ * Usage in CI: run BEFORE `playwright test`. Idempotent — fast on
41
+ * cache hit (~3s for the login flow), faster on no-op.
42
+ */
43
+
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+ const {
47
+ authStatePath,
48
+ envVarNames,
49
+ sanitizeRole,
50
+ } = require('../utils/setupAuth');
51
+
52
+ const FATAL_EXIT = 1;
53
+ const DEFAULT_LOGIN_FILE = 'tests/appliqation/auth/login.ts';
54
+
55
+ function parseArgs(argv) {
56
+ const args = {};
57
+ for (let i = 2; i < argv.length; i++) {
58
+ const arg = argv[i];
59
+ if (!arg.startsWith('--')) continue;
60
+ const key = arg.slice(2);
61
+ const next = argv[i + 1];
62
+ if (next && !next.startsWith('--')) {
63
+ args[key] = next;
64
+ i++;
65
+ } else {
66
+ args[key] = true;
67
+ }
68
+ }
69
+ return args;
70
+ }
71
+
72
+ function fail(message) {
73
+ console.error(`✗ ${message}`);
74
+ process.exit(FATAL_EXIT);
75
+ }
76
+
77
+ /**
78
+ * Resolve the customer's login.ts path. Priority:
79
+ * 1. --login-file CLI flag (explicit override)
80
+ * 2. APPQ_LOGIN_FILE env var (CI override without changing CLI args)
81
+ * 3. Convention: tests/appliqation/auth/login.ts relative to CWD
82
+ *
83
+ * Throws clearly if the resolved path doesn't exist — customers who
84
+ * haven't created the file yet need a path-specific error, not a
85
+ * cryptic dynamic-import failure.
86
+ */
87
+ function resolveLoginFile(args) {
88
+ const candidate = args['login-file']
89
+ || process.env.APPQ_LOGIN_FILE
90
+ || DEFAULT_LOGIN_FILE;
91
+ const absolute = path.isAbsolute(candidate)
92
+ ? candidate
93
+ : path.resolve(process.cwd(), candidate);
94
+ if (!fs.existsSync(absolute)) {
95
+ fail(
96
+ `Login file not found: ${absolute}\n`
97
+ + ` Create it (default path: ${DEFAULT_LOGIN_FILE}) and export your login flow as a default export:\n`
98
+ + `\n`
99
+ + ` import { defineLogin } from '@appliqation/automation-sdk/login';\n`
100
+ + ` export default defineLogin(async (page, { username, password, baseURL }) => {\n`
101
+ + ` await page.goto('/login');\n`
102
+ + ` // ... your login flow\n`
103
+ + ` });\n`
104
+ );
105
+ }
106
+ return absolute;
107
+ }
108
+
109
+ /**
110
+ * Dynamic-import the login file and return the default export. Handles
111
+ * both ESM (`export default ...`) and CommonJS (`module.exports = ...`)
112
+ * — the SDK's defineLogin is identity, so customers writing either
113
+ * style end up with a function we can call.
114
+ *
115
+ * For .ts files, requires the customer's repo to have a TS loader
116
+ * registered (tsx, ts-node, esbuild-node-loader). Most Playwright
117
+ * projects do via @playwright/test which transpiles TS on the fly,
118
+ * but the auth-setup CLI runs OUTSIDE the Playwright runner so we
119
+ * need the loader explicitly. We try `tsx` first (lightweight, the
120
+ * most common choice), then `ts-node`, then fail with a clear hint.
121
+ */
122
+ async function loadLoginFunction(filePath) {
123
+ const ext = path.extname(filePath).toLowerCase();
124
+ if (ext === '.ts' || ext === '.tsx' || ext === '.mts' || ext === '.cts') {
125
+ let registered = false;
126
+ try {
127
+ require('tsx/cjs');
128
+ registered = true;
129
+ } catch {
130
+ try {
131
+ require('ts-node/register/transpile-only');
132
+ registered = true;
133
+ } catch { /* fall through */ }
134
+ }
135
+ if (!registered) {
136
+ fail(
137
+ `Login file is TypeScript (${path.basename(filePath)}) but no TS loader was found.\n`
138
+ + ` Install one as a dev dependency:\n`
139
+ + ` npm install --save-dev tsx\n`
140
+ + ` Or rename your login file to .js (CommonJS) / .mjs (ESM).`
141
+ );
142
+ }
143
+ }
144
+
145
+ let mod;
146
+ try {
147
+ mod = require(filePath);
148
+ } catch (requireErr) {
149
+ // ESM fallback — `require` of an ESM file throws ERR_REQUIRE_ESM.
150
+ try {
151
+ mod = await import(filePath);
152
+ } catch (importErr) {
153
+ fail(
154
+ `Failed to load ${filePath}:\n`
155
+ + ` CommonJS error: ${requireErr.message}\n`
156
+ + ` ESM error: ${importErr.message}`
157
+ );
158
+ }
159
+ }
160
+
161
+ const fn = mod && (mod.default || mod);
162
+ if (typeof fn !== 'function') {
163
+ fail(
164
+ `${filePath} did not default-export a function.\n`
165
+ + ` Expected:\n`
166
+ + ` export default defineLogin(async (page, ctx) => { ... });\n`
167
+ + ` Got: ${typeof fn}`
168
+ );
169
+ }
170
+ return fn;
171
+ }
172
+
173
+ async function main() {
174
+ const args = parseArgs(process.argv);
175
+
176
+ const project_id = args['project-id']
177
+ || process.env.APPLIQATION_PROJECT_KEY
178
+ || null;
179
+ const role = sanitizeRole(args.role || 'default');
180
+
181
+ if (!project_id) {
182
+ fail(
183
+ 'Missing --project-id. Pass it as a flag or set APPLIQATION_PROJECT_KEY '
184
+ + 'in your env (the downloaded .env from your project settings includes it).'
185
+ );
186
+ }
187
+
188
+ const baseURL = (process.env.APPLIQATION_SUT_BASE_URL || '').replace(/\/$/, '');
189
+ if (!baseURL) {
190
+ fail(
191
+ 'APPLIQATION_SUT_BASE_URL env var is not set. This is your SUT (System Under Test) URL '
192
+ + '— e.g. https://staging.acme.com — that the login flow runs against. Add it to your .env / CI secrets.'
193
+ );
194
+ }
195
+
196
+ const { username: userVar, password: pwdVar } = envVarNames({ project_id, role });
197
+ const username = process.env[userVar];
198
+ const password = process.env[pwdVar];
199
+ if (!username || !password) {
200
+ fail(
201
+ `Missing credential env vars: ${userVar} and/or ${pwdVar}.\n`
202
+ + ` Download the .env template from your project settings in Appliqation,\n`
203
+ + ` fill in the values, and add them to your CI secrets.`
204
+ );
205
+ }
206
+
207
+ const loginFile = resolveLoginFile(args);
208
+ console.log(`→ Loading login flow from ${path.relative(process.cwd(), loginFile)}`);
209
+ const loginFn = await loadLoginFunction(loginFile);
210
+
211
+ // Lazy-require playwright — it's a peer dep, not always installed.
212
+ let chromium;
213
+ try {
214
+ ({ chromium } = require('playwright'));
215
+ } catch {
216
+ fail(
217
+ 'Playwright is not installed. Run `npm install --save-dev playwright` '
218
+ + '(or @playwright/test) and try again.'
219
+ );
220
+ }
221
+
222
+ const targetPath = authStatePath({ project_id, role });
223
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
224
+
225
+ console.log(`→ Launching Chromium for login as role "${role}" against ${baseURL}`);
226
+ const browser = await chromium.launch({ headless: true });
227
+ const context = await browser.newContext({
228
+ baseURL,
229
+ ignoreHTTPSErrors: true,
230
+ });
231
+ const page = await context.newPage();
232
+
233
+ try {
234
+ await loginFn(page, { username, password, role, baseURL });
235
+ await context.storageState({ path: targetPath });
236
+ console.log(`✓ Auth state saved to ${targetPath}`);
237
+ console.log(` Tests using setupAuth({ project_id: ${project_id}, role: '${role}' }) will pick this up.`);
238
+ } catch (err) {
239
+ fail(`Login failed: ${err.message}`);
240
+ } finally {
241
+ await browser.close();
242
+ }
243
+ }
244
+
245
+ main().catch((err) => fail(err.message || String(err)));
package/src/index.d.ts CHANGED
@@ -331,3 +331,60 @@ export const logger: {
331
331
  * ```
332
332
  */
333
333
  export function mapAppqUuid(testInfo: any, uuid: string): void;
334
+
335
+ /**
336
+ * Returns the canonical Playwright `storageState` file path for a given
337
+ * Appliqation project + role. The same path resolves regardless of
338
+ * execution context, so the same generated/authored test runs unchanged
339
+ * in:
340
+ * - Appliqation's executor (LoginHelper writes the file at this path)
341
+ * - Customer CI / local (the `appq-auth-setup` CLI writes the file)
342
+ *
343
+ * Does NOT perform login itself — only computes the path. Pair with
344
+ * `npx appq-auth-setup --project-id X --role Y` (or Appliqation's
345
+ * executor) to populate the file before tests run.
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * import { test } from '@playwright/test';
350
+ * import { mapAppqUuid, setupAuth } from '@appliqation/automation-sdk/utils';
351
+ *
352
+ * test.use({ storageState: setupAuth({ project_id: 126, role: 'default' }) });
353
+ *
354
+ * test('manager dashboard loads', async ({ page }, testInfo) => {
355
+ * mapAppqUuid(testInfo, '1141-...');
356
+ * await page.goto('/dashboard'); // already authenticated
357
+ * });
358
+ * ```
359
+ */
360
+ export function setupAuth(options: { project_id: number | string; role?: string }): string;
361
+
362
+ /**
363
+ * Computes the canonical storageState file path. Same as setupAuth, but
364
+ * usable by tooling that needs the path without going through Playwright
365
+ * (e.g. the appq-auth-setup CLI writes to this path).
366
+ */
367
+ export function authStatePath(options: { project_id: number | string; role?: string }): string;
368
+
369
+ /**
370
+ * Base directory for storageState files. Defaults to `~/.appq-auth/`;
371
+ * overridable via APPQ_AUTH_STATE_DIR env (Appliqation's executor sets
372
+ * this per-run for tenant isolation).
373
+ */
374
+ export function authStateDir(): string;
375
+
376
+ /**
377
+ * The canonical env-var names the CLI looks up for credentials, given
378
+ * a project_id + role. Customers populate these from the `.env` template
379
+ * downloaded from Appliqation's project settings.
380
+ */
381
+ export function envVarNames(options: { project_id: number | string; role?: string }): {
382
+ username: string;
383
+ password: string;
384
+ };
385
+
386
+ /**
387
+ * Normalize a role name to lowercase with only alphanumerics, underscore,
388
+ * and hyphen. Mirrors the role-name validation in the Appliqation UI.
389
+ */
390
+ export function sanitizeRole(role: string): string;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * TypeScript definitions for @appliqation/automation-sdk/login.
3
+ *
4
+ * Customers writing `tests/appliqation/auth/login.ts` import these
5
+ * types for autocomplete + compile-time checking on the login flow
6
+ * function shape. Same file gets imported (untyped, dynamically) by
7
+ * the Appliqation executor's LoginHelper and the appq-auth-setup CLI.
8
+ */
9
+
10
+ import type { Page } from '@playwright/test';
11
+
12
+ /**
13
+ * Per-call context passed to the customer's login function. Sourced
14
+ * differently in each runtime:
15
+ *
16
+ * - Appliqation executor: username + password from AWS Secrets
17
+ * Manager (read-only IAM); baseURL from the project's env
18
+ * configuration; role from snapshot.auth_role.
19
+ * - Customer CI (`npx appq-auth-setup`): username + password from
20
+ * APPQ_PROJECT_<id>_<ROLE>_USERNAME/PASSWORD env vars; baseURL
21
+ * from APPLIQATION_SUT_BASE_URL env var; role from --role flag.
22
+ */
23
+ export interface LoginContext {
24
+ /** Username / email for the configured role. */
25
+ username: string;
26
+ /** Password for the configured role. */
27
+ password: string;
28
+ /**
29
+ * Role name as configured in Project Settings. Lets the customer
30
+ * branch login flow per role (e.g. an admin's login may include a
31
+ * "Switch to admin view" step that other roles skip).
32
+ */
33
+ role: string;
34
+ /**
35
+ * SUT base URL for the env this run targets. Use `await
36
+ * page.goto('/login')` — Playwright resolves the relative path
37
+ * against the context's baseURL, which is set from this value.
38
+ * Provided here for cases where the customer needs the full URL
39
+ * (e.g. cross-origin OAuth redirects).
40
+ */
41
+ baseURL: string;
42
+ }
43
+
44
+ /**
45
+ * Customer's login function. Should leave the page in an
46
+ * authenticated state — typically by navigating to the login URL,
47
+ * filling credentials, submitting, and waiting for a post-login URL
48
+ * or visible-element indicator. The caller (LoginHelper or
49
+ * appq-auth-setup) captures the resulting storageState immediately
50
+ * after this resolves.
51
+ *
52
+ * Throw on failure. The caller will surface the error verbatim in
53
+ * the executor's run audit / the CLI's exit code.
54
+ */
55
+ export type LoginFunction = (
56
+ page: Page,
57
+ ctx: LoginContext,
58
+ ) => Promise<void>;
59
+
60
+ /**
61
+ * Identity helper for type inference. Wrap your login function with
62
+ * this so your IDE provides autocomplete on the LoginContext fields:
63
+ *
64
+ * ```typescript
65
+ * import { defineLogin } from '@appliqation/automation-sdk/login';
66
+ *
67
+ * export default defineLogin(async (page, { username, password }) => {
68
+ * await page.goto('/login');
69
+ * await page.getByLabel('Email').fill(username);
70
+ * await page.getByLabel('Password').fill(password);
71
+ * await page.getByRole('button', { name: 'Sign in' }).click();
72
+ * await page.waitForURL('**\/dashboard');
73
+ * });
74
+ * ```
75
+ */
76
+ export function defineLogin<F extends LoginFunction>(fn: F): F;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @appliqation/automation-sdk/login — types + helpers for customer-
3
+ * supplied login flows.
4
+ *
5
+ * Customers define their SUT's login flow as a Playwright function in
6
+ * their own repo (canonical path: `tests/appliqation/auth/login.ts`).
7
+ * The function is imported by:
8
+ * 1. Appliqation's executor (LoginHelper) — pulled from MongoDB
9
+ * after a GitHub-webhook upsert into automan_canonical_scripts
10
+ * with script_type:'login', dynamic-imported per role per cache
11
+ * miss.
12
+ * 2. Customer CI (`npx appq-auth-setup`) — dynamic-imported from
13
+ * disk, run with role-specific creds from env vars, output a
14
+ * Playwright storageState file all tests load via
15
+ * test.use({ storageState: setupAuth({ ... }) }).
16
+ *
17
+ * Same source file, two readers. Both run the customer's own code.
18
+ *
19
+ * Why offline GitOps and not in-app upload/edit:
20
+ * - Customer's IDE + lint + PR review + git history come for free
21
+ * - No in-app editor to build, no sandboxing concern beyond what we
22
+ * already have for test scripts
23
+ * - The login function is just Playwright code — it should live next
24
+ * to the Playwright tests
25
+ *
26
+ * The selector-fill model in the legacy auth_config schema (login_url
27
+ * + 3 CSS selectors + success_indicator) only handled the simplest
28
+ * form-login case. SSO / MFA / OAuth / multi-step / captcha all
29
+ * require custom code anyway. Customer login.ts handles everything.
30
+ */
31
+
32
+ /**
33
+ * Identity helper that gives type inference + auto-complete for
34
+ * customers writing their login.ts. Mirrors Playwright's
35
+ * `defineConfig` pattern — pure function, no runtime side-effects.
36
+ *
37
+ * @template {LoginFunction} F
38
+ * @param {F} fn
39
+ * @returns {F}
40
+ */
41
+ function defineLogin(fn) {
42
+ if (typeof fn !== 'function') {
43
+ throw new TypeError('defineLogin: argument must be a function');
44
+ }
45
+ return fn;
46
+ }
47
+
48
+ module.exports = { defineLogin };
@@ -74,8 +74,10 @@ function getRunIdFromCli() {
74
74
  */
75
75
  class AppliqationReporter {
76
76
  constructor(config = {}) {
77
- // 1. Check enablement FIRST — --appq flag is the only trigger
78
- this.appqEnabled = process.argv.includes('--appq');
77
+ // 1. Check enablement — --appq CLI flag or APPQ_ENABLED env var
78
+ this.appqEnabled = process.argv.includes('--appq')
79
+ || process.env.APPQ_ENABLED === 'true'
80
+ || process.env.APPQ_ENABLED === '1';
79
81
 
80
82
  this.config = {
81
83
  autoCreateRun: true,
@@ -6,6 +6,13 @@ const UuidValidator = require('./UuidValidator');
6
6
  const PayloadBuilder = require('./PayloadBuilder');
7
7
  const RunDataNormalizer = require('./RunDataNormalizer');
8
8
  const { mapAppqUuid } = require('./mapAppqUuid');
9
+ const {
10
+ setupAuth,
11
+ authStatePath,
12
+ authStateDir,
13
+ envVarNames,
14
+ sanitizeRole,
15
+ } = require('./setupAuth');
9
16
  const logger = require('./logger');
10
17
 
11
18
  module.exports = {
@@ -13,5 +20,10 @@ module.exports = {
13
20
  PayloadBuilder,
14
21
  RunDataNormalizer,
15
22
  mapAppqUuid,
23
+ setupAuth,
24
+ authStatePath,
25
+ authStateDir,
26
+ envVarNames,
27
+ sanitizeRole,
16
28
  logger
17
29
  };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * setupAuth — portable Playwright storageState resolution.
3
+ *
4
+ * Returns a deterministic file path. The same path resolves to the same
5
+ * file regardless of execution context, so the same generated/authored
6
+ * test script runs unchanged in:
7
+ *
8
+ * - Appliqation's executor (the LoginHelper writes the file at this
9
+ * path during validation prep, using AWS Secrets Manager-backed
10
+ * credentials)
11
+ *
12
+ * - Customer CI / local (the `appq-auth-setup` CLI writes the file at
13
+ * this path, using credentials from env vars per the convention
14
+ * downloaded from Project Settings → "download .env for CI")
15
+ *
16
+ * setupAuth() does NOT perform login itself — that's the CLI's job (CI)
17
+ * or the executor's job (Appliqation cloud). It only computes the path.
18
+ *
19
+ * Usage in a Playwright test file:
20
+ *
21
+ * const { mapAppqUuid, setupAuth } = require('@appliqation/automation-sdk/utils');
22
+ *
23
+ * test.use({ storageState: setupAuth({ project_id: 126, role: 'default' }) });
24
+ *
25
+ * test('...', async ({ page }, testInfo) => {
26
+ * mapAppqUuid(testInfo, '<uuid>');
27
+ * // body — already authenticated
28
+ * });
29
+ */
30
+
31
+ const path = require('path');
32
+ const os = require('os');
33
+
34
+ /**
35
+ * Default base directory for storageState files. Overridable via
36
+ * APPQ_AUTH_STATE_DIR env var (Appliqation executor sets this per-run
37
+ * for tenant isolation; customers normally don't need to).
38
+ */
39
+ function authStateDir() {
40
+ return process.env.APPQ_AUTH_STATE_DIR
41
+ || path.join(os.homedir(), '.appq-auth');
42
+ }
43
+
44
+ /**
45
+ * Compute the canonical storageState path for a given project + role.
46
+ * Pure function — no I/O. Both the SDK CLI and the Appliqation executor
47
+ * call this same function so they always agree on the location.
48
+ */
49
+ function authStatePath({ project_id, role = 'default' }) {
50
+ if (project_id === undefined || project_id === null || project_id === '') {
51
+ throw new Error('authStatePath requires project_id');
52
+ }
53
+ const safeRole = sanitizeRole(role);
54
+ return path.join(authStateDir(), `project-${project_id}-${safeRole}.json`);
55
+ }
56
+
57
+ /**
58
+ * The public entry point. Returns a string suitable for Playwright's
59
+ * `storageState` option (a file path).
60
+ */
61
+ function setupAuth({ project_id, role = 'default' } = {}) {
62
+ if (project_id === undefined || project_id === null || project_id === '') {
63
+ throw new Error(
64
+ 'setupAuth requires project_id. Generated test files include this '
65
+ + 'as a literal; for hand-authored tests, copy from your project URL '
66
+ + 'in Appliqation.'
67
+ );
68
+ }
69
+ return authStatePath({ project_id, role });
70
+ }
71
+
72
+ /**
73
+ * Lowercase + alphanumeric/underscore/hyphen only. Mirrors the role-name
74
+ * validation in the Appliqation project settings UI so paths line up.
75
+ */
76
+ function sanitizeRole(role) {
77
+ return String(role).toLowerCase().replace(/[^a-z0-9_-]/g, '_');
78
+ }
79
+
80
+ /**
81
+ * Env-var name convention for the CLI (and any other consumer) to look
82
+ * up credentials. Matches what the "download .env for CI" feature in
83
+ * Appliqation generates. Exported so the CLI and the SDK never drift.
84
+ */
85
+ function envVarNames({ project_id, role }) {
86
+ const safeRole = sanitizeRole(role).toUpperCase();
87
+ return {
88
+ username: `APPQ_PROJECT_${project_id}_${safeRole}_USERNAME`,
89
+ password: `APPQ_PROJECT_${project_id}_${safeRole}_PASSWORD`,
90
+ };
91
+ }
92
+
93
+ module.exports = {
94
+ setupAuth,
95
+ authStatePath,
96
+ authStateDir,
97
+ envVarNames,
98
+ sanitizeRole,
99
+ };