@devicecloud.dev/dcd 5.6.0-beta.3 → 5.6.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 (39) hide show
  1. package/README.md +4 -2
  2. package/dist/commands/artifacts.d.ts +1 -1
  3. package/dist/commands/artifacts.js +1 -1
  4. package/dist/commands/cloud.d.ts +5 -5
  5. package/dist/commands/cloud.js +14 -6
  6. package/dist/commands/switch-org.js +5 -1
  7. package/dist/commands/upload.js +5 -1
  8. package/dist/commands/whoami.d.ts +7 -1
  9. package/dist/commands/whoami.js +26 -2
  10. package/dist/config/flags/device.flags.d.ts +3 -3
  11. package/dist/config/flags/device.flags.js +5 -3
  12. package/dist/config/flags/execution.flags.d.ts +1 -1
  13. package/dist/config/flags/execution.flags.js +1 -1
  14. package/dist/config/flags/output.flags.d.ts +1 -1
  15. package/dist/config/flags/output.flags.js +1 -1
  16. package/dist/constants.d.ts +5 -5
  17. package/dist/gateways/api-gateway.js +2 -1
  18. package/dist/gateways/realtime-gateway.d.ts +7 -0
  19. package/dist/gateways/realtime-gateway.js +12 -1
  20. package/dist/mcp/context.d.ts +14 -3
  21. package/dist/mcp/context.js +15 -3
  22. package/dist/mcp/tools/download-artifacts.js +9 -6
  23. package/dist/mcp/tools/list-devices.js +1 -1
  24. package/dist/mcp/tools/run-cloud-test.js +19 -3
  25. package/dist/services/device-validation.service.d.ts +17 -2
  26. package/dist/services/device-validation.service.js +93 -12
  27. package/dist/services/notices.service.js +3 -0
  28. package/dist/services/report-download.service.js +9 -3
  29. package/dist/services/results-polling.service.d.ts +21 -0
  30. package/dist/services/results-polling.service.js +55 -3
  31. package/dist/services/test-submission.service.d.ts +11 -0
  32. package/dist/services/test-submission.service.js +12 -5
  33. package/dist/types/domain/auth.types.d.ts +6 -0
  34. package/dist/types/domain/device.types.d.ts +18 -4
  35. package/dist/types/domain/device.types.js +17 -3
  36. package/dist/types/generated/schema.types.d.ts +127 -167
  37. package/dist/utils/auth.d.ts +33 -0
  38. package/dist/utils/auth.js +61 -2
  39. package/package.json +2 -2
@@ -1649,9 +1649,11 @@ export interface components {
1649
1649
  /** @enum {string} */
1650
1650
  iOSVersion?: "17" | "18" | "26" | "27";
1651
1651
  /** @enum {string} */
1652
- iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "ipad-pro-6th-gen";
1652
+ iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "iphone-17" | "iphone-air" | "iphone-18-pro" | "iphone-18-pro-max" | "ipad-pro-6th-gen" | "ipad-pro-m5-11" | "ipad-pro-m5-13";
1653
1653
  platform?: string;
1654
1654
  googlePlay?: boolean;
1655
+ /** @description Cancel the still-queued tests of the previous run from the same CI context. The context is derived from the run metadata (gh_repo/bb_repo, gh_branch/gh_pr_number or their bb_ twins, and gh_check_name); without it nothing is cancelled. Tests already running are left to finish. */
1656
+ cancelPrevious?: boolean;
1655
1657
  config: string;
1656
1658
  name?: string;
1657
1659
  /** @enum {string} */
@@ -1689,9 +1691,11 @@ export interface components {
1689
1691
  /** @enum {string} */
1690
1692
  iOSVersion?: "17" | "18" | "26" | "27";
1691
1693
  /** @enum {string} */
1692
- iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "ipad-pro-6th-gen";
1694
+ iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "iphone-17" | "iphone-air" | "iphone-18-pro" | "iphone-18-pro-max" | "ipad-pro-6th-gen" | "ipad-pro-m5-11" | "ipad-pro-m5-13";
1693
1695
  platform?: string;
1694
1696
  googlePlay?: boolean;
1697
+ /** @description Cancel the still-queued tests of the previous run from the same CI context. The context is derived from the run metadata (gh_repo/bb_repo, gh_branch/gh_pr_number or their bb_ twins, and gh_check_name); without it nothing is cancelled. Tests already running are left to finish. */
1698
+ cancelPrevious?: boolean;
1695
1699
  config: string;
1696
1700
  name?: string;
1697
1701
  /** @enum {string} */
@@ -1740,9 +1744,11 @@ export interface components {
1740
1744
  /** @enum {string} */
1741
1745
  iOSVersion?: "17" | "18" | "26" | "27";
1742
1746
  /** @enum {string} */
1743
- iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "ipad-pro-6th-gen";
1747
+ iOSDevice?: "iphone-14" | "iphone-15" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "iphone-17" | "iphone-air" | "iphone-18-pro" | "iphone-18-pro-max" | "ipad-pro-6th-gen" | "ipad-pro-m5-11" | "ipad-pro-m5-13";
1744
1748
  platform?: string;
1745
1749
  googlePlay?: boolean;
1750
+ /** @description Cancel the still-queued tests of the previous run from the same CI context. The context is derived from the run metadata (gh_repo/bb_repo, gh_branch/gh_pr_number or their bb_ twins, and gh_check_name); without it nothing is cancelled. Tests already running are left to finish. */
1751
+ cancelPrevious?: boolean;
1746
1752
  config: string;
1747
1753
  name?: string;
1748
1754
  /** @enum {string} */
@@ -1878,6 +1884,7 @@ export interface components {
1878
1884
  retry_of?: number;
1879
1885
  fail_reason?: string;
1880
1886
  duration_seconds?: number;
1887
+ cancellation_reason?: string;
1881
1888
  simulator_name?: string;
1882
1889
  config?: Record<string, never>;
1883
1890
  };
@@ -2188,6 +2195,8 @@ export interface operations {
2188
2195
  message?: string;
2189
2196
  success?: boolean;
2190
2197
  cancelledCount?: number;
2198
+ /** @description Credits returned for tests that had not started yet. 0 when nothing was refundable. See the billing docs for the rate. */
2199
+ refundedAmount?: number;
2191
2200
  };
2192
2201
  };
2193
2202
  };
@@ -2949,174 +2958,125 @@ export interface operations {
2949
2958
  * "statusCode": 200,
2950
2959
  * "data": {
2951
2960
  * "ios": {
2952
- * "iphone-14": {
2953
- * "name": "iPhone 14",
2954
- * "versions": [
2955
- * "17",
2956
- * "18"
2957
- * ],
2958
- * "deprecated": false
2959
- * },
2960
- * "iphone-15": {
2961
- * "name": "iPhone 15",
2962
- * "versions": [
2963
- * "17"
2964
- * ],
2965
- * "deprecated": false
2966
- * },
2967
- * "iphone-16": {
2968
- * "name": "iPhone 16",
2969
- * "versions": [
2970
- * "18",
2971
- * "26"
2972
- * ],
2973
- * "deprecated": false
2974
- * },
2975
- * "iphone-16-plus": {
2976
- * "name": "iPhone 16 Plus",
2977
- * "versions": [
2978
- * "26"
2979
- * ],
2980
- * "deprecated": false
2981
- * },
2982
- * "iphone-16-pro": {
2983
- * "name": "iPhone 16 Pro",
2984
- * "versions": [
2985
- * "18",
2986
- * "26"
2987
- * ],
2988
- * "deprecated": false
2989
- * },
2990
- * "iphone-16-pro-max": {
2991
- * "name": "iPhone 16 Pro Max",
2992
- * "versions": [
2993
- * "18",
2994
- * "26"
2995
- * ],
2996
- * "deprecated": false
2997
- * },
2998
- * "ipad-pro-6th-gen": {
2999
- * "name": "iPad Pro (6th gen)",
3000
- * "versions": [
3001
- * "18",
3002
- * "26"
3003
- * ],
3004
- * "deprecated": false
3005
- * }
2961
+ * "iphone-14": [
2962
+ * "17",
2963
+ * "18"
2964
+ * ],
2965
+ * "iphone-15": [
2966
+ * "17"
2967
+ * ],
2968
+ * "iphone-16": [
2969
+ * "18",
2970
+ * "26",
2971
+ * "27"
2972
+ * ],
2973
+ * "iphone-16-plus": [
2974
+ * "26",
2975
+ * "27"
2976
+ * ],
2977
+ * "iphone-16-pro": [
2978
+ * "18",
2979
+ * "26",
2980
+ * "27"
2981
+ * ],
2982
+ * "iphone-16-pro-max": [
2983
+ * "18",
2984
+ * "26",
2985
+ * "27"
2986
+ * ],
2987
+ * "iphone-17": [
2988
+ * "26",
2989
+ * "27"
2990
+ * ],
2991
+ * "iphone-air": [
2992
+ * "26",
2993
+ * "27"
2994
+ * ],
2995
+ * "iphone-18-pro": [
2996
+ * "27"
2997
+ * ],
2998
+ * "iphone-18-pro-max": [
2999
+ * "27"
3000
+ * ],
3001
+ * "ipad-pro-6th-gen": [
3002
+ * "18",
3003
+ * "26",
3004
+ * "27"
3005
+ * ],
3006
+ * "ipad-pro-m5-11": [
3007
+ * "26",
3008
+ * "27"
3009
+ * ],
3010
+ * "ipad-pro-m5-13": [
3011
+ * "26",
3012
+ * "27"
3013
+ * ]
3006
3014
  * },
3007
3015
  * "android": {
3008
- * "pixel-6": {
3009
- * "name": "Pixel 6",
3010
- * "apiLevels": [
3011
- * "29",
3012
- * "30",
3013
- * "31",
3014
- * "32",
3015
- * "33",
3016
- * "34",
3017
- * "35",
3018
- * "36",
3019
- * "37"
3020
- * ],
3021
- * "deprecated": false
3022
- * },
3023
- * "pixel-6-pro": {
3024
- * "name": "Pixel 6 Pro",
3025
- * "apiLevels": [
3026
- * "33",
3027
- * "35"
3028
- * ],
3029
- * "deprecated": false
3030
- * },
3031
- * "pixel-7": {
3032
- * "name": "Pixel 7",
3033
- * "apiLevels": [
3034
- * "33",
3035
- * "34",
3036
- * "35",
3037
- * "36",
3038
- * "37"
3039
- * ],
3040
- * "deprecated": false
3041
- * },
3042
- * "pixel-7-pro": {
3043
- * "name": "Pixel 7 Pro",
3044
- * "apiLevels": [
3045
- * "33",
3046
- * "34",
3047
- * "35",
3048
- * "36",
3049
- * "37"
3050
- * ],
3051
- * "deprecated": false
3052
- * },
3053
- * "pixel-8": {
3054
- * "name": "Pixel 8",
3055
- * "apiLevels": [
3056
- * "34",
3057
- * "35",
3058
- * "36",
3059
- * "37"
3060
- * ],
3061
- * "deprecated": false
3062
- * },
3063
- * "pixel-10": {
3064
- * "name": "Pixel 10",
3065
- * "apiLevels": [
3066
- * "36",
3067
- * "37"
3068
- * ],
3069
- * "deprecated": false
3070
- * },
3071
- * "pixel-10-pro": {
3072
- * "name": "Pixel 10 Pro",
3073
- * "apiLevels": [
3074
- * "36",
3075
- * "37"
3076
- * ],
3077
- * "deprecated": false
3078
- * },
3079
- * "pixel-10-pro-xl": {
3080
- * "name": "Pixel 10 Pro XL",
3081
- * "apiLevels": [
3082
- * "36",
3083
- * "37"
3084
- * ],
3085
- * "deprecated": false
3086
- * },
3087
- * "pixel-10-pro-fold": {
3088
- * "name": "Pixel 10 Pro Fold",
3089
- * "apiLevels": [
3090
- * "36",
3091
- * "37"
3092
- * ],
3093
- * "deprecated": false
3094
- * },
3095
- * "pixel-11": {
3096
- * "name": "Pixel 11",
3097
- * "apiLevels": [
3098
- * "37"
3099
- * ],
3100
- * "deprecated": false
3101
- * },
3102
- * "generic-tablet": {
3103
- * "name": "Generic Tablet",
3104
- * "apiLevels": [
3105
- * "33",
3106
- * "36",
3107
- * "37"
3108
- * ],
3109
- * "deprecated": false
3110
- * }
3016
+ * "pixel-6": [
3017
+ * "29",
3018
+ * "30",
3019
+ * "31",
3020
+ * "32",
3021
+ * "33",
3022
+ * "34",
3023
+ * "35",
3024
+ * "36",
3025
+ * "37"
3026
+ * ],
3027
+ * "pixel-6-pro": [
3028
+ * "33",
3029
+ * "35"
3030
+ * ],
3031
+ * "pixel-7": [
3032
+ * "33",
3033
+ * "34",
3034
+ * "35",
3035
+ * "36",
3036
+ * "37"
3037
+ * ],
3038
+ * "pixel-7-pro": [
3039
+ * "33",
3040
+ * "34",
3041
+ * "35",
3042
+ * "36",
3043
+ * "37"
3044
+ * ],
3045
+ * "pixel-8": [
3046
+ * "34",
3047
+ * "35",
3048
+ * "36",
3049
+ * "37"
3050
+ * ],
3051
+ * "pixel-10": [
3052
+ * "36",
3053
+ * "37"
3054
+ * ],
3055
+ * "pixel-10-pro": [
3056
+ * "36",
3057
+ * "37"
3058
+ * ],
3059
+ * "pixel-10-pro-xl": [
3060
+ * "36",
3061
+ * "37"
3062
+ * ],
3063
+ * "pixel-10-pro-fold": [
3064
+ * "36",
3065
+ * "37"
3066
+ * ],
3067
+ * "pixel-11": [
3068
+ * "37"
3069
+ * ],
3070
+ * "generic-tablet": [
3071
+ * "33",
3072
+ * "36",
3073
+ * "37"
3074
+ * ]
3111
3075
  * },
3112
3076
  * "androidPlay": {
3113
- * "pixel-7": {
3114
- * "name": "Pixel 7 (Google Play)",
3115
- * "apiLevels": [
3116
- * "34"
3117
- * ],
3118
- * "deprecated": false
3119
- * }
3077
+ * "pixel-7": [
3078
+ * "34"
3079
+ * ]
3120
3080
  * },
3121
3081
  * "maestro": {
3122
3082
  * "supportedVersions": [
@@ -10,7 +10,40 @@ export interface ResolveAuthOptions {
10
10
  */
11
11
  sessionOnly?: boolean;
12
12
  }
13
+ /**
14
+ * Which API key, if any, outranks the stored session — the precedence
15
+ * resolveAuth applies. For the session-only commands (`whoami`, `switch-org`)
16
+ * to say that what they show is not what other commands will authenticate as.
17
+ */
18
+ export declare function apiKeyOverride(apiKeyFlag?: string): '--api-key' | 'DEVICE_CLOUD_API_KEY' | undefined;
13
19
  export declare function resolveAuth(opts: ResolveAuthOptions): Promise<AuthContext>;
20
+ /**
21
+ * True when a `dcd login` (bearer) context's access token has expired or will
22
+ * within the refresh window. API-key contexts never expire.
23
+ */
24
+ export declare function isAuthExpiring(auth: AuthContext, nowSeconds?: number): boolean;
25
+ /**
26
+ * Keep a long-running command's credential usable. An API-key context, or a
27
+ * session that isn't near expiry, comes back unchanged. Otherwise the stored
28
+ * session is re-read and, if it is expiring too, refreshed under the same lock
29
+ * resolveAuth uses — so a session another `dcd` process has already rotated
30
+ * is picked up rather than raced for.
31
+ *
32
+ * Only the token changes: the org the command started with is kept, so an
33
+ * org switched in another terminal mid-run can't redirect an in-flight poll
34
+ * at an upload that org doesn't own.
35
+ *
36
+ * @throws SessionRefreshError when Supabase refuses the refresh (`definitive`
37
+ * when only `dcd login` can fix it), or CliError when there is no stored
38
+ * session any more (e.g. after `dcd logout`).
39
+ */
40
+ export declare function refreshAuth(auth: AuthContext): Promise<AuthContext>;
41
+ /**
42
+ * Whether a refreshAuth failure is final for this process — the refresh token
43
+ * was refused, or the session is gone — as opposed to a network blip that the
44
+ * next attempt may get past.
45
+ */
46
+ export declare function isDefinitiveAuthFailure(error: unknown): boolean;
14
47
  /**
15
48
  * Remove only the (definitively dead) session from the stored config, keeping
16
49
  * env/api_url/org so a re-login lands back in the same environment. With the
@@ -4,8 +4,9 @@
4
4
  * Supabase session (refreshed if near expiry). Throws CliError if none found.
5
5
  *
6
6
  * The refresh path writes the rotated tokens back to disk atomically. Callers
7
- * use the returned AuthContext for the duration of the command — one resolve
8
- * per invocation, not per request.
7
+ * resolve once per invocation, not per request — except where a process can
8
+ * outlive the session's access token (the MCP server, a long `dcd cloud`
9
+ * poll), which re-checks with isAuthExpiring / refreshAuth below.
9
10
  */
10
11
  import { closeSync, openSync, rmSync, statSync } from 'node:fs';
11
12
  import { ENVIRONMENTS } from '../config/environments.js';
@@ -19,6 +20,18 @@ const REFRESH_SKEW_SECONDS = 60;
19
20
  const LOCK_STALE_MS = 10_000;
20
21
  const LOCK_WAIT_MS = 10_000;
21
22
  const LOCK_POLL_MS = 250;
23
+ /**
24
+ * Which API key, if any, outranks the stored session — the precedence
25
+ * resolveAuth applies. For the session-only commands (`whoami`, `switch-org`)
26
+ * to say that what they show is not what other commands will authenticate as.
27
+ */
28
+ export function apiKeyOverride(apiKeyFlag) {
29
+ if (apiKeyFlag?.trim())
30
+ return '--api-key';
31
+ if (process.env.DEVICE_CLOUD_API_KEY?.trim())
32
+ return 'DEVICE_CLOUD_API_KEY';
33
+ return undefined;
34
+ }
22
35
  export async function resolveAuth(opts) {
23
36
  if (!opts.sessionOnly) {
24
37
  const flag = opts.apiKeyFlag?.trim();
@@ -59,6 +72,7 @@ export async function resolveAuth(opts) {
59
72
  mode: 'bearer',
60
73
  accessToken: session.access_token,
61
74
  env: config.env,
75
+ expiresAt: session.expires_at,
62
76
  orgId: config.current_org_id,
63
77
  userEmail: session.user_email,
64
78
  headers: {
@@ -69,6 +83,51 @@ export async function resolveAuth(opts) {
69
83
  telemetry.configure({ auth, apiUrl: config.api_url });
70
84
  return auth;
71
85
  }
86
+ /**
87
+ * True when a `dcd login` (bearer) context's access token has expired or will
88
+ * within the refresh window. API-key contexts never expire.
89
+ */
90
+ export function isAuthExpiring(auth, nowSeconds = Math.floor(Date.now() / 1000)) {
91
+ return (auth.mode === 'bearer' &&
92
+ auth.expiresAt !== undefined &&
93
+ auth.expiresAt <= nowSeconds + REFRESH_SKEW_SECONDS);
94
+ }
95
+ /**
96
+ * Keep a long-running command's credential usable. An API-key context, or a
97
+ * session that isn't near expiry, comes back unchanged. Otherwise the stored
98
+ * session is re-read and, if it is expiring too, refreshed under the same lock
99
+ * resolveAuth uses — so a session another `dcd` process has already rotated
100
+ * is picked up rather than raced for.
101
+ *
102
+ * Only the token changes: the org the command started with is kept, so an
103
+ * org switched in another terminal mid-run can't redirect an in-flight poll
104
+ * at an upload that org doesn't own.
105
+ *
106
+ * @throws SessionRefreshError when Supabase refuses the refresh (`definitive`
107
+ * when only `dcd login` can fix it), or CliError when there is no stored
108
+ * session any more (e.g. after `dcd logout`).
109
+ */
110
+ export async function refreshAuth(auth) {
111
+ if (!isAuthExpiring(auth))
112
+ return auth;
113
+ const fresh = await resolveAuth({ apiKeyFlag: undefined, sessionOnly: true });
114
+ if (!auth.orgId || fresh.orgId === auth.orgId)
115
+ return fresh;
116
+ return {
117
+ ...fresh,
118
+ orgId: auth.orgId,
119
+ headers: { ...fresh.headers, 'x-dcd-org': auth.orgId },
120
+ };
121
+ }
122
+ /**
123
+ * Whether a refreshAuth failure is final for this process — the refresh token
124
+ * was refused, or the session is gone — as opposed to a network blip that the
125
+ * next attempt may get past.
126
+ */
127
+ export function isDefinitiveAuthFailure(error) {
128
+ return ((error instanceof SessionRefreshError && error.definitive) ||
129
+ error instanceof CliError);
130
+ }
72
131
  /**
73
132
  * Refresh the stored session under a config-adjacent lockfile so two
74
133
  * concurrent `dcd` invocations (CI matrices) can't both consume the same
package/package.json CHANGED
@@ -31,7 +31,7 @@
31
31
  "chai": "^6.2.2",
32
32
  "eslint": "^10.5.0",
33
33
  "eslint-config-prettier": "^10.1.8",
34
- "eslint-plugin-unicorn": "^74.0.0",
34
+ "eslint-plugin-unicorn": "^76.0.0",
35
35
  "husky": "^9.1.7",
36
36
  "mocha": "^12.0.0",
37
37
  "prettier": "^3.8.4",
@@ -69,7 +69,7 @@
69
69
  "test:unit": "node scripts/test-runner.mjs --unit",
70
70
  "typecheck": "tsc --noEmit -p tsconfig.test.json"
71
71
  },
72
- "version": "5.6.0-beta.3",
72
+ "version": "5.6.0",
73
73
  "bugs": {
74
74
  "url": "https://discord.gg/gm3mJwcNw8"
75
75
  },