@showrun/core 0.1.8 → 0.1.9

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 (61) hide show
  1. package/dist/__tests__/dsl-validation.test.js +185 -0
  2. package/dist/__tests__/httpReplay.test.js +62 -0
  3. package/dist/__tests__/proxy.test.d.ts +2 -0
  4. package/dist/__tests__/proxy.test.d.ts.map +1 -0
  5. package/dist/__tests__/proxy.test.js +117 -0
  6. package/dist/__tests__/registry-client.test.d.ts +2 -0
  7. package/dist/__tests__/registry-client.test.d.ts.map +1 -0
  8. package/dist/__tests__/registry-client.test.js +228 -0
  9. package/dist/browserLauncher.d.ts +10 -0
  10. package/dist/browserLauncher.d.ts.map +1 -1
  11. package/dist/browserLauncher.js +17 -3
  12. package/dist/dsl/builders.d.ts +22 -1
  13. package/dist/dsl/builders.d.ts.map +1 -1
  14. package/dist/dsl/builders.js +23 -0
  15. package/dist/dsl/interpreter.d.ts +5 -0
  16. package/dist/dsl/interpreter.d.ts.map +1 -1
  17. package/dist/dsl/interpreter.js +1 -0
  18. package/dist/dsl/stepHandlers.d.ts +3 -0
  19. package/dist/dsl/stepHandlers.d.ts.map +1 -1
  20. package/dist/dsl/stepHandlers.js +62 -2
  21. package/dist/dsl/types.d.ts +53 -1
  22. package/dist/dsl/types.d.ts.map +1 -1
  23. package/dist/dsl/validation.d.ts.map +1 -1
  24. package/dist/dsl/validation.js +90 -1
  25. package/dist/httpReplay.d.ts +5 -0
  26. package/dist/httpReplay.d.ts.map +1 -1
  27. package/dist/httpReplay.js +46 -2
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -0
  31. package/dist/jsonPackValidator.js +1 -1
  32. package/dist/proxy/index.d.ts +4 -0
  33. package/dist/proxy/index.d.ts.map +1 -0
  34. package/dist/proxy/index.js +3 -0
  35. package/dist/proxy/oxylabs.d.ts +15 -0
  36. package/dist/proxy/oxylabs.d.ts.map +1 -0
  37. package/dist/proxy/oxylabs.js +34 -0
  38. package/dist/proxy/proxyService.d.ts +34 -0
  39. package/dist/proxy/proxyService.d.ts.map +1 -0
  40. package/dist/proxy/proxyService.js +69 -0
  41. package/dist/proxy/types.d.ts +59 -0
  42. package/dist/proxy/types.d.ts.map +1 -0
  43. package/dist/proxy/types.js +4 -0
  44. package/dist/registry/client.d.ts +33 -0
  45. package/dist/registry/client.d.ts.map +1 -0
  46. package/dist/registry/client.js +261 -0
  47. package/dist/registry/index.d.ts +4 -0
  48. package/dist/registry/index.d.ts.map +1 -0
  49. package/dist/registry/index.js +3 -0
  50. package/dist/registry/tokenStore.d.ts +13 -0
  51. package/dist/registry/tokenStore.d.ts.map +1 -0
  52. package/dist/registry/tokenStore.js +54 -0
  53. package/dist/registry/types.d.ts +110 -0
  54. package/dist/registry/types.d.ts.map +1 -0
  55. package/dist/registry/types.js +4 -0
  56. package/dist/runner.d.ts +4 -0
  57. package/dist/runner.d.ts.map +1 -1
  58. package/dist/runner.js +12 -4
  59. package/dist/types.d.ts +6 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/package.json +1 -1
@@ -15,22 +15,50 @@ const DEFAULT_TIMEOUT_MS = 30_000;
15
15
  // HTTP-only compatibility check
16
16
  // ---------------------------------------------------------------------------
17
17
  /** Step types that require DOM access for data extraction (force browser mode). */
18
- const DOM_EXTRACTION_STEPS = new Set(['extract_text', 'extract_title', 'extract_attribute']);
18
+ const DOM_EXTRACTION_STEPS = new Set(['extract_text', 'extract_title', 'extract_attribute', 'dom_scrape']);
19
+ /**
20
+ * Step types that are silently skipped in HTTP mode.
21
+ * Must match HTTP_MODE_SKIP_STEPS in stepHandlers.ts.
22
+ */
23
+ const HTTP_SKIPPED_STEPS = new Set([
24
+ 'navigate', 'click', 'fill', 'select_option', 'press_key',
25
+ 'upload_file', 'wait_for', 'assert', 'frame', 'new_tab',
26
+ 'switch_tab', 'network_find', 'dom_scrape',
27
+ ]);
28
+ /** Check if a value contains Nunjucks template expressions. */
29
+ function containsTemplate(value) {
30
+ if (typeof value === 'string')
31
+ return value.includes('{{');
32
+ if (Array.isArray(value))
33
+ return value.some(containsTemplate);
34
+ if (value && typeof value === 'object') {
35
+ return Object.values(value).some(containsTemplate);
36
+ }
37
+ return false;
38
+ }
19
39
  /**
20
40
  * Check whether a flow can run in HTTP-only mode.
21
41
  *
22
42
  * Requirements:
23
43
  * 1. Every `network_replay` step has a corresponding, non-stale snapshot.
24
44
  * 2. No DOM extraction steps exist in the flow.
45
+ * 3. No skipped steps contain dynamic templates — templates in skipped steps
46
+ * would never be evaluated, so the snapshot replays stale data.
25
47
  */
26
48
  export function isFlowHttpCompatible(steps, snapshots) {
27
49
  if (!snapshots)
28
50
  return false;
29
- // Check for DOM extraction steps
30
51
  for (const step of steps) {
52
+ // DOM extraction steps force browser mode
31
53
  if (DOM_EXTRACTION_STEPS.has(step.type)) {
32
54
  return false;
33
55
  }
56
+ // Steps skipped in HTTP mode must not contain templates — those templates
57
+ // affect what data the API returns but would never be evaluated, causing
58
+ // the snapshot to replay stale/wrong data regardless of input values.
59
+ if (HTTP_SKIPPED_STEPS.has(step.type) && containsTemplate(step.params)) {
60
+ return false;
61
+ }
34
62
  }
35
63
  // Check that every network_replay step has a valid snapshot
36
64
  const replaySteps = steps.filter((s) => s.type === 'network_replay');
@@ -70,6 +98,22 @@ export async function replayFromSnapshot(snapshot, inputs, vars, options) {
70
98
  if (body && method !== 'GET' && method !== 'HEAD') {
71
99
  fetchOptions.body = body;
72
100
  }
101
+ // When proxy is provided, create a ProxyAgent dispatcher for undici-backed fetch.
102
+ // undici is bundled with Node but may not have separate type declarations.
103
+ if (options?.proxy) {
104
+ try {
105
+ // @ts-expect-error undici types may not be installed; runtime import is fine
106
+ const undiciModule = await import('undici');
107
+ const ProxyAgentClass = undiciModule.ProxyAgent;
108
+ if (ProxyAgentClass) {
109
+ const proxyUrl = options.proxy.server.replace('://', `://${encodeURIComponent(options.proxy.username)}:${encodeURIComponent(options.proxy.password)}@`);
110
+ fetchOptions.dispatcher = new ProxyAgentClass(proxyUrl);
111
+ }
112
+ }
113
+ catch {
114
+ console.warn('[httpReplay] Failed to load undici ProxyAgent, making direct request');
115
+ }
116
+ }
73
117
  try {
74
118
  const response = await fetch(url, fetchOptions);
75
119
  const responseBody = await response.text();
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * from './packVersioning.js';
13
13
  export * from './config.js';
14
14
  export * from './requestSnapshot.js';
15
15
  export * from './httpReplay.js';
16
+ export * from './proxy/index.js';
16
17
  export * from './storage/index.js';
17
18
  export * from './dsl/types.js';
18
19
  export * from './dsl/builders.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAGhC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAGhC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -13,6 +13,8 @@ export * from './packVersioning.js';
13
13
  export * from './config.js';
14
14
  export * from './requestSnapshot.js';
15
15
  export * from './httpReplay.js';
16
+ // Proxy exports
17
+ export * from './proxy/index.js';
16
18
  // Storage exports
17
19
  export * from './storage/index.js';
18
20
  // DSL exports
@@ -2,7 +2,7 @@ import { validateFlow, ValidationError } from './dsl/validation.js';
2
2
  /** Step types that produce collectible output via an "out" parameter */
3
3
  const STEPS_WITH_OUT = new Set([
4
4
  'extract_title', 'extract_text', 'extract_attribute',
5
- 'network_replay', 'network_extract',
5
+ 'network_replay', 'network_extract', 'dom_scrape',
6
6
  ]);
7
7
  /**
8
8
  * Validates that collectibles referenced in flow steps exist
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './oxylabs.js';
3
+ export * from './proxyService.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './oxylabs.js';
3
+ export * from './proxyService.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * OxyLabs residential proxy provider.
3
+ *
4
+ * Endpoint: pr.oxylabs.io:7777
5
+ * Username format:
6
+ * Random: customer-{USERNAME}[-cc-{COUNTRY}]
7
+ * Session: customer-{USERNAME}[-cc-{COUNTRY}]-sessid-{UUID}-sesstime-{MIN}
8
+ */
9
+ import type { ProxyConfig, ProxyCredentials, ProxyProvider, ResolvedProxy } from './types.js';
10
+ export declare class OxylabsProvider implements ProxyProvider {
11
+ readonly name = "oxylabs";
12
+ resolve(config: ProxyConfig, credentials: ProxyCredentials): ResolvedProxy;
13
+ requiredCredentialKeys(): string[];
14
+ }
15
+ //# sourceMappingURL=oxylabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oxylabs.d.ts","sourceRoot":"","sources":["../../src/proxy/oxylabs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAK9F,qBAAa,eAAgB,YAAW,aAAa;IACnD,QAAQ,CAAC,IAAI,aAAa;IAE1B,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,GAAG,aAAa;IAqB1E,sBAAsB,IAAI,MAAM,EAAE;CAGnC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * OxyLabs residential proxy provider.
3
+ *
4
+ * Endpoint: pr.oxylabs.io:7777
5
+ * Username format:
6
+ * Random: customer-{USERNAME}[-cc-{COUNTRY}]
7
+ * Session: customer-{USERNAME}[-cc-{COUNTRY}]-sessid-{UUID}-sesstime-{MIN}
8
+ */
9
+ import { randomUUID } from 'crypto';
10
+ const OXYLABS_ENDPOINT = 'http://pr.oxylabs.io:7777';
11
+ const DEFAULT_SESSION_DURATION_MINUTES = 10;
12
+ export class OxylabsProvider {
13
+ name = 'oxylabs';
14
+ resolve(config, credentials) {
15
+ const mode = config.mode ?? 'session';
16
+ let username = `customer-${credentials.username}`;
17
+ if (config.country) {
18
+ username += `-cc-${config.country.toUpperCase()}`;
19
+ }
20
+ if (mode === 'session') {
21
+ const sessionId = randomUUID().replace(/-/g, '');
22
+ const duration = config.sessionDurationMinutes ?? DEFAULT_SESSION_DURATION_MINUTES;
23
+ username += `-sessid-${sessionId}-sesstime-${duration}`;
24
+ }
25
+ return {
26
+ server: OXYLABS_ENDPOINT,
27
+ username,
28
+ password: credentials.password,
29
+ };
30
+ }
31
+ requiredCredentialKeys() {
32
+ return ['USERNAME', 'PASSWORD'];
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Proxy service: provider registry and resolution.
3
+ *
4
+ * Reads credentials from environment variables:
5
+ * SHOWRUN_PROXY_USERNAME, SHOWRUN_PROXY_PASSWORD
6
+ * Provider from config or SHOWRUN_PROXY_PROVIDER env var (default: 'oxylabs').
7
+ *
8
+ * Graceful fallback: if env vars are missing, returns null (no proxy) instead of throwing.
9
+ */
10
+ import type { ProxyConfig, ProxyProvider, ResolvedProxy } from './types.js';
11
+ /**
12
+ * Register a custom proxy provider.
13
+ */
14
+ export declare function registerProxyProvider(provider: ProxyProvider): void;
15
+ /**
16
+ * Get a registered proxy provider by name.
17
+ */
18
+ export declare function getProxyProvider(name: string): ProxyProvider | undefined;
19
+ /**
20
+ * List all registered proxy provider names.
21
+ */
22
+ export declare function listProxyProviders(): string[];
23
+ /**
24
+ * Resolve a proxy configuration into concrete connection details.
25
+ *
26
+ * Returns `null` when:
27
+ * - config is undefined or disabled
28
+ * - required env vars are missing (graceful fallback, logs warning)
29
+ *
30
+ * Throws when:
31
+ * - provider name is unknown
32
+ */
33
+ export declare function resolveProxy(config: ProxyConfig | undefined): ResolvedProxy | null;
34
+ //# sourceMappingURL=proxyService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxyService.d.ts","sourceRoot":"","sources":["../../src/proxy/proxyService.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAoB,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAW9F;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAEnE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAExE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C;AAaD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,IAAI,CAsBlF"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Proxy service: provider registry and resolution.
3
+ *
4
+ * Reads credentials from environment variables:
5
+ * SHOWRUN_PROXY_USERNAME, SHOWRUN_PROXY_PASSWORD
6
+ * Provider from config or SHOWRUN_PROXY_PROVIDER env var (default: 'oxylabs').
7
+ *
8
+ * Graceful fallback: if env vars are missing, returns null (no proxy) instead of throwing.
9
+ */
10
+ import { OxylabsProvider } from './oxylabs.js';
11
+ // ── Provider registry ──────────────────────────────────────────────────
12
+ const providers = new Map();
13
+ // Register OxyLabs by default
14
+ const defaultProvider = new OxylabsProvider();
15
+ providers.set(defaultProvider.name, defaultProvider);
16
+ /**
17
+ * Register a custom proxy provider.
18
+ */
19
+ export function registerProxyProvider(provider) {
20
+ providers.set(provider.name, provider);
21
+ }
22
+ /**
23
+ * Get a registered proxy provider by name.
24
+ */
25
+ export function getProxyProvider(name) {
26
+ return providers.get(name);
27
+ }
28
+ /**
29
+ * List all registered proxy provider names.
30
+ */
31
+ export function listProxyProviders() {
32
+ return [...providers.keys()];
33
+ }
34
+ // ── Credential helpers ─────────────────────────────────────────────────
35
+ function readCredentialsFromEnv() {
36
+ const username = process.env.SHOWRUN_PROXY_USERNAME;
37
+ const password = process.env.SHOWRUN_PROXY_PASSWORD;
38
+ if (!username || !password)
39
+ return null;
40
+ return { username, password };
41
+ }
42
+ // ── Resolution ─────────────────────────────────────────────────────────
43
+ /**
44
+ * Resolve a proxy configuration into concrete connection details.
45
+ *
46
+ * Returns `null` when:
47
+ * - config is undefined or disabled
48
+ * - required env vars are missing (graceful fallback, logs warning)
49
+ *
50
+ * Throws when:
51
+ * - provider name is unknown
52
+ */
53
+ export function resolveProxy(config) {
54
+ if (!config || !config.enabled)
55
+ return null;
56
+ const providerName = config.provider ?? process.env.SHOWRUN_PROXY_PROVIDER ?? 'oxylabs';
57
+ const provider = providers.get(providerName);
58
+ if (!provider) {
59
+ throw new Error(`Unknown proxy provider "${providerName}". Registered providers: ${listProxyProviders().join(', ')}`);
60
+ }
61
+ const credentials = readCredentialsFromEnv();
62
+ if (!credentials) {
63
+ console.warn(`[proxy] Proxy enabled but credentials not configured. ` +
64
+ `Set SHOWRUN_PROXY_USERNAME and SHOWRUN_PROXY_PASSWORD environment variables. ` +
65
+ `Running without proxy.`);
66
+ return null;
67
+ }
68
+ return provider.resolve(config, credentials);
69
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Proxy configuration types for browser and HTTP request proxying.
3
+ */
4
+ /**
5
+ * Proxy mode determines how proxy sessions are managed:
6
+ * - 'session': Sticky IP for the duration of the session (default)
7
+ * - 'random': Rotating IP per request
8
+ */
9
+ export type ProxyMode = 'session' | 'random';
10
+ /**
11
+ * Proxy configuration stored in taskpack.json under `browser.proxy`.
12
+ * Persisted by the agent's `set_proxy` tool.
13
+ */
14
+ export interface ProxyConfig {
15
+ /** Whether proxy is enabled */
16
+ enabled: boolean;
17
+ /** Proxy mode: sticky session or random rotation */
18
+ mode?: ProxyMode;
19
+ /** Provider name (default: 'oxylabs') */
20
+ provider?: string;
21
+ /** Two-letter ISO country code for geo-targeting */
22
+ country?: string;
23
+ /** Session duration in minutes (for session mode) */
24
+ sessionDurationMinutes?: number;
25
+ }
26
+ /**
27
+ * Resolved proxy credentials ready for Playwright / fetch.
28
+ * Maps directly to Playwright's `proxy` launch option.
29
+ */
30
+ export interface ResolvedProxy {
31
+ server: string;
32
+ username: string;
33
+ password: string;
34
+ }
35
+ /**
36
+ * Credentials supplied to a proxy provider (from env vars).
37
+ */
38
+ export interface ProxyCredentials {
39
+ username: string;
40
+ password: string;
41
+ }
42
+ /**
43
+ * Interface for pluggable proxy providers.
44
+ * Implement this to add support for a new proxy service.
45
+ */
46
+ export interface ProxyProvider {
47
+ /** Provider name (e.g. 'oxylabs') */
48
+ readonly name: string;
49
+ /**
50
+ * Resolve proxy config + credentials into a concrete proxy connection.
51
+ */
52
+ resolve(config: ProxyConfig, credentials: ProxyCredentials): ResolvedProxy;
53
+ /**
54
+ * List of env var suffixes this provider needs (for display/validation).
55
+ * e.g. ['USERNAME', 'PASSWORD'] → expects SHOWRUN_PROXY_USERNAME, SHOWRUN_PROXY_PASSWORD
56
+ */
57
+ requiredCredentialKeys(): string[];
58
+ }
59
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/proxy/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,oDAAoD;IACpD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,GAAG,aAAa,CAAC;IAE3E;;;OAGG;IACH,sBAAsB,IAAI,MAAM,EAAE,CAAC;CACpC"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Proxy configuration types for browser and HTTP request proxying.
3
+ */
4
+ export {};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Registry client for ShowRun pack registry.
3
+ *
4
+ * Communicates with the registry REST API to authenticate, publish, search,
5
+ * and install task packs. Uses the global token store for persistence and
6
+ * automatically refreshes access tokens before they expire.
7
+ */
8
+ import type { IRegistryClient, DeviceCodeResponse, DevicePollResult, UserProfile, PublishParams, PublishResult, PackSummary, PaginatedResponse, SearchQuery } from './types.js';
9
+ export declare class RegistryError extends Error {
10
+ readonly status: number;
11
+ readonly body?: unknown | undefined;
12
+ constructor(message: string, status: number, body?: unknown | undefined);
13
+ }
14
+ export declare class RegistryClient implements IRegistryClient {
15
+ private readonly registryUrl;
16
+ constructor(registryUrl?: string);
17
+ startDeviceLogin(): Promise<DeviceCodeResponse>;
18
+ pollDeviceLogin(deviceCode: string): Promise<DevicePollResult>;
19
+ logout(): Promise<void>;
20
+ whoami(): Promise<UserProfile>;
21
+ isAuthenticated(): boolean;
22
+ publishPack(params: PublishParams): Promise<PublishResult>;
23
+ searchPacks(query: SearchQuery): Promise<PaginatedResponse<PackSummary>>;
24
+ installPack(slug: string, destDir: string, version?: string): Promise<void>;
25
+ private request;
26
+ /**
27
+ * Get a valid access token, refreshing if it's about to expire (within 60s).
28
+ * If refresh fails, clears tokens and throws.
29
+ */
30
+ private getValidAccessToken;
31
+ private refreshAccessToken;
32
+ }
33
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/registry/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAGhB,WAAW,EACX,aAAa,EACb,aAAa,EACb,WAAW,EAEX,iBAAiB,EACjB,WAAW,EAEZ,MAAM,YAAY,CAAC;AAMpB,qBAAa,aAAc,SAAQ,KAAK;aAGpB,MAAM,EAAE,MAAM;aACd,IAAI,CAAC,EAAE,OAAO;gBAF9B,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,OAAO,YAAA;CAKjC;AAwBD,qBAAa,cAAe,YAAW,eAAe;IACpD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,WAAW,CAAC,EAAE,MAAM;IAc1B,gBAAgB,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAS/C,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA+C9D,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAIvB,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC;IAIpC,eAAe,IAAI,OAAO;IAOpB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAiE1D,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAWxE,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YA0BnE,OAAO;IAwCrB;;;OAGG;YACW,mBAAmB;YAkCnB,kBAAkB;CAcjC"}
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Registry client for ShowRun pack registry.
3
+ *
4
+ * Communicates with the registry REST API to authenticate, publish, search,
5
+ * and install task packs. Uses the global token store for persistence and
6
+ * automatically refreshes access tokens before they expire.
7
+ */
8
+ import { join } from 'path';
9
+ import { TaskPackLoader } from '../loader.js';
10
+ import { readJsonFile, ensureDir, writeTaskPackManifest, writeFlowJson } from '../packUtils.js';
11
+ import { loadTokens, saveTokens, clearTokens } from './tokenStore.js';
12
+ // ── Error class ───────────────────────────────────────────────────────────
13
+ export class RegistryError extends Error {
14
+ status;
15
+ body;
16
+ constructor(message, status, body) {
17
+ super(message);
18
+ this.status = status;
19
+ this.body = body;
20
+ this.name = 'RegistryError';
21
+ }
22
+ }
23
+ // ── JWT helpers ───────────────────────────────────────────────────────────
24
+ function decodeJwtPayload(token) {
25
+ try {
26
+ const parts = token.split('.');
27
+ if (parts.length !== 3)
28
+ return null;
29
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
30
+ return JSON.parse(payload);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ function tokenExpiresWithin(token, seconds) {
37
+ const payload = decodeJwtPayload(token);
38
+ if (!payload || typeof payload.exp !== 'number')
39
+ return true; // assume expired
40
+ const expiresAt = payload.exp * 1000;
41
+ return Date.now() + seconds * 1000 >= expiresAt;
42
+ }
43
+ // ── RegistryClient ────────────────────────────────────────────────────────
44
+ export class RegistryClient {
45
+ registryUrl;
46
+ constructor(registryUrl) {
47
+ const url = registryUrl || process.env.SHOWRUN_REGISTRY_URL;
48
+ if (!url) {
49
+ throw new RegistryError('Registry not configured. Set SHOWRUN_REGISTRY_URL or registry.url in config.json.', 0);
50
+ }
51
+ // Strip trailing slash
52
+ this.registryUrl = url.replace(/\/+$/, '');
53
+ }
54
+ // ── Auth (OAuth Device Flow — RFC 8628) ────────────────────────────────
55
+ async startDeviceLogin() {
56
+ return this.request('POST', '/api/auth/device', {}, false);
57
+ }
58
+ async pollDeviceLogin(deviceCode) {
59
+ const url = `${this.registryUrl}/api/auth/device/token`;
60
+ const res = await fetch(url, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ deviceCode }),
64
+ });
65
+ if (res.status === 428 || res.status === 400) {
66
+ // authorization_pending or slow_down — user hasn't approved yet
67
+ const body = await res.json().catch(() => ({}));
68
+ const error = body.error;
69
+ if (error === 'expired') {
70
+ return { status: 'expired' };
71
+ }
72
+ return { status: 'pending' };
73
+ }
74
+ if (!res.ok) {
75
+ let errBody;
76
+ try {
77
+ errBody = await res.json();
78
+ }
79
+ catch {
80
+ errBody = '';
81
+ }
82
+ const message = (errBody && typeof errBody === 'object' && 'message' in errBody
83
+ ? String(errBody.message)
84
+ : null) || `Device token request failed (${res.status})`;
85
+ throw new RegistryError(message, res.status, errBody);
86
+ }
87
+ const data = (await res.json());
88
+ // Store tokens
89
+ saveTokens({
90
+ accessToken: data.accessToken,
91
+ refreshToken: data.refreshToken,
92
+ user: data.user,
93
+ registryUrl: this.registryUrl,
94
+ savedAt: new Date().toISOString(),
95
+ });
96
+ return {
97
+ status: 'complete',
98
+ accessToken: data.accessToken,
99
+ refreshToken: data.refreshToken,
100
+ user: data.user,
101
+ };
102
+ }
103
+ async logout() {
104
+ clearTokens();
105
+ }
106
+ async whoami() {
107
+ return this.request('GET', '/api/auth/me', undefined, true);
108
+ }
109
+ isAuthenticated() {
110
+ const tokens = loadTokens();
111
+ return tokens !== null && tokens.registryUrl === this.registryUrl;
112
+ }
113
+ // ── Packs ─────────────────────────────────────────────────────────────
114
+ async publishPack(params) {
115
+ const { packPath, slug: userSlug, visibility = 'public', changelog } = params;
116
+ const warnings = [];
117
+ // Load local pack
118
+ const manifest = TaskPackLoader.loadManifest(packPath);
119
+ const flowPath = join(packPath, 'flow.json');
120
+ const flowData = readJsonFile(flowPath);
121
+ const slug = userSlug || manifest.id;
122
+ // Try to get existing pack; create if 404
123
+ let created = false;
124
+ try {
125
+ await this.request('GET', `/api/packs/${slug}`, undefined, true);
126
+ }
127
+ catch (err) {
128
+ if (err instanceof RegistryError && err.status === 404) {
129
+ await this.request('POST', '/api/packs', {
130
+ slug,
131
+ name: manifest.name,
132
+ description: manifest.description || '',
133
+ visibility,
134
+ }, true);
135
+ created = true;
136
+ }
137
+ else {
138
+ throw err;
139
+ }
140
+ }
141
+ // Publish version
142
+ const versionData = await this.request('POST', `/api/packs/${slug}/versions`, {
143
+ version: manifest.version,
144
+ manifest: {
145
+ id: manifest.id,
146
+ name: manifest.name,
147
+ version: manifest.version,
148
+ description: manifest.description,
149
+ kind: manifest.kind,
150
+ },
151
+ flow: flowData,
152
+ changelog,
153
+ }, true);
154
+ return {
155
+ slug,
156
+ version: versionData.version,
157
+ created,
158
+ warnings,
159
+ };
160
+ }
161
+ async searchPacks(query) {
162
+ const params = new URLSearchParams();
163
+ if (query.q)
164
+ params.set('q', query.q);
165
+ if (query.page)
166
+ params.set('page', String(query.page));
167
+ if (query.limit)
168
+ params.set('limit', String(query.limit));
169
+ const qs = params.toString();
170
+ const path = `/api/packs${qs ? `?${qs}` : ''}`;
171
+ return this.request('GET', path, undefined, false);
172
+ }
173
+ async installPack(slug, destDir, version) {
174
+ // Get pack detail
175
+ const detail = await this.request('GET', `/api/packs/${slug}`, undefined, false);
176
+ // Determine version to install
177
+ const targetVersion = version || detail.latestVersion;
178
+ if (!targetVersion) {
179
+ throw new RegistryError(`Pack "${slug}" has no published versions`, 404);
180
+ }
181
+ // Get version data (manifest + flow)
182
+ const versionData = await this.request('GET', `/api/packs/${slug}/versions/${targetVersion}`, undefined, false);
183
+ // Write to local directory
184
+ const packDir = join(destDir, slug);
185
+ ensureDir(packDir);
186
+ writeTaskPackManifest(packDir, versionData.manifest);
187
+ writeFlowJson(packDir, versionData.flow);
188
+ }
189
+ // ── Internal request helper ───────────────────────────────────────────
190
+ async request(method, path, body, authenticated = false) {
191
+ const url = `${this.registryUrl}${path}`;
192
+ const headers = {
193
+ 'Content-Type': 'application/json',
194
+ };
195
+ if (authenticated) {
196
+ const accessToken = await this.getValidAccessToken();
197
+ headers['Authorization'] = `Bearer ${accessToken}`;
198
+ }
199
+ const res = await fetch(url, {
200
+ method,
201
+ headers,
202
+ body: body ? JSON.stringify(body) : undefined,
203
+ });
204
+ if (!res.ok) {
205
+ let errBody;
206
+ try {
207
+ errBody = await res.json();
208
+ }
209
+ catch {
210
+ errBody = await res.text().catch(() => '');
211
+ }
212
+ const message = (errBody && typeof errBody === 'object' && 'message' in errBody
213
+ ? String(errBody.message)
214
+ : null) || `Registry request failed: ${method} ${path} (${res.status})`;
215
+ throw new RegistryError(message, res.status, errBody);
216
+ }
217
+ return (await res.json());
218
+ }
219
+ /**
220
+ * Get a valid access token, refreshing if it's about to expire (within 60s).
221
+ * If refresh fails, clears tokens and throws.
222
+ */
223
+ async getValidAccessToken() {
224
+ const auth = loadTokens();
225
+ if (!auth) {
226
+ throw new RegistryError('Not logged in. Run `showrun registry login` first.', 401);
227
+ }
228
+ // Check if token is still valid (with 60s buffer)
229
+ if (!tokenExpiresWithin(auth.accessToken, 60)) {
230
+ return auth.accessToken;
231
+ }
232
+ // Token expired or about to expire — refresh
233
+ try {
234
+ const data = await this.refreshAccessToken(auth);
235
+ // Update stored tokens with new access token
236
+ saveTokens({
237
+ ...auth,
238
+ accessToken: data.accessToken,
239
+ savedAt: new Date().toISOString(),
240
+ });
241
+ return data.accessToken;
242
+ }
243
+ catch {
244
+ // Refresh failed — clear tokens, user needs to log in again
245
+ clearTokens();
246
+ throw new RegistryError('Session expired. Run `showrun registry login` to authenticate again.', 401);
247
+ }
248
+ }
249
+ async refreshAccessToken(auth) {
250
+ const url = `${this.registryUrl}/api/auth/refresh`;
251
+ const res = await fetch(url, {
252
+ method: 'POST',
253
+ headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify({ refreshToken: auth.refreshToken }),
255
+ });
256
+ if (!res.ok) {
257
+ throw new RegistryError('Token refresh failed', res.status);
258
+ }
259
+ return (await res.json());
260
+ }
261
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './tokenStore.js';
3
+ export { RegistryClient, RegistryError } from './client.js';
4
+ //# sourceMappingURL=index.d.ts.map