crewly 1.20.132 → 1.20.133
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/config/constants.ts +49 -0
- package/config/index.ts +2 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts +56 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js +114 -0
- package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +25 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +73 -55
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts +3 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +9 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts +115 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js +160 -0
- package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +8 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/backend/config/constants.d.ts +48 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +48 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/backend/config/index.d.ts +2 -2
- package/dist/backend/config/index.d.ts.map +1 -1
- package/dist/backend/config/index.js +2 -2
- package/dist/backend/config/index.js.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
- package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
- package/dist/cli/cli/src/commands/cloud.d.ts +45 -25
- package/dist/cli/cli/src/commands/cloud.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/cloud.js +153 -47
- package/dist/cli/cli/src/commands/cloud.js.map +1 -1
- package/dist/cli/cli/src/commands/onboard-checklist.js +3 -3
- package/dist/cli/cli/src/commands/onboard-checklist.js.map +1 -1
- package/dist/cli/cli/src/index.js +6 -4
- package/dist/cli/cli/src/index.js.map +1 -1
- package/dist/cli/cli/src/utils/cloud-submit.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/cloud-submit.js +14 -5
- package/dist/cli/cli/src/utils/cloud-submit.js.map +1 -1
- package/dist/cli/config/constants.d.ts +48 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +48 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/dist/cli/config/index.d.ts +2 -2
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +2 -2
- package/dist/cli/config/index.js.map +1 -1
- package/frontend/dist/assets/{index-5b98bf77.js → index-e6c403b6.js} +265 -265
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud device-code pairing client.
|
|
3
|
+
*
|
|
4
|
+
* Talks to crewly-auth's `/api/cloud/device/*` endpoints so this machine can
|
|
5
|
+
* sign in to the owner's Crewly account without anyone at it:
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link startCloudDevicePairing} — get a secret `deviceCode`, a short
|
|
8
|
+
* `userCode` (`ABCD-2345`) and the link the owner opens
|
|
9
|
+
* (`crewlyai.com/cloud/pair?code=ABCD-2345`).
|
|
10
|
+
* 2. {@link waitForCloudDeviceApproval} — poll at the Cloud's interval
|
|
11
|
+
* (backing off on `slow_down` and rate limits) until the owner approves,
|
|
12
|
+
* denies, or the pairing expires. On approval the Cloud hands over the
|
|
13
|
+
* token pair exactly once.
|
|
14
|
+
*
|
|
15
|
+
* Shared by `crewly cloud login` (CLI) and {@link CloudDevicePairingService}
|
|
16
|
+
* (backend, for `/setup` and Settings → Cloud). No state; the device code
|
|
17
|
+
* never leaves the caller's memory.
|
|
18
|
+
*
|
|
19
|
+
* @module services/cloud/cloud-device-pairing.client
|
|
20
|
+
*/
|
|
21
|
+
/** `fetch` signature (injectable for tests). */
|
|
22
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
23
|
+
/** What `start` returns. */
|
|
24
|
+
export interface DevicePairingStartResult {
|
|
25
|
+
/** Secret the machine polls with — never show or log it */
|
|
26
|
+
deviceCode: string;
|
|
27
|
+
/** Short code the owner checks on the approve page */
|
|
28
|
+
userCode: string;
|
|
29
|
+
/** Approve page with the code filled in (show / encode as QR) */
|
|
30
|
+
verificationUrl: string;
|
|
31
|
+
/** Approve page without the code (for typing it by hand) */
|
|
32
|
+
verificationUri?: string;
|
|
33
|
+
/** Pairing lifetime (seconds) */
|
|
34
|
+
expiresIn: number;
|
|
35
|
+
/** Minimum seconds between polls */
|
|
36
|
+
interval: number;
|
|
37
|
+
}
|
|
38
|
+
/** The token pair handed over on approval. */
|
|
39
|
+
export interface DevicePairingCredentials {
|
|
40
|
+
token: string;
|
|
41
|
+
refreshToken: string;
|
|
42
|
+
tier: string;
|
|
43
|
+
email?: string;
|
|
44
|
+
}
|
|
45
|
+
/** One poll's answer. */
|
|
46
|
+
export type DevicePairingPollResult = {
|
|
47
|
+
status: 'authorization_pending' | 'slow_down';
|
|
48
|
+
interval?: number;
|
|
49
|
+
} | {
|
|
50
|
+
status: 'expired' | 'denied';
|
|
51
|
+
} | {
|
|
52
|
+
status: 'approved';
|
|
53
|
+
credentials: DevicePairingCredentials;
|
|
54
|
+
};
|
|
55
|
+
/** How waiting ended. */
|
|
56
|
+
export type DevicePairingOutcome = {
|
|
57
|
+
status: 'approved';
|
|
58
|
+
credentials: DevicePairingCredentials;
|
|
59
|
+
} | {
|
|
60
|
+
status: 'expired' | 'denied' | 'cancelled';
|
|
61
|
+
};
|
|
62
|
+
/** A request the Cloud refused (or could not answer). */
|
|
63
|
+
export declare class DevicePairingError extends Error {
|
|
64
|
+
readonly status: number;
|
|
65
|
+
readonly retryable: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* @param message - Human-readable reason (never contains a secret)
|
|
68
|
+
* @param status - HTTP status, 0 when the Cloud was unreachable
|
|
69
|
+
* @param retryable - True for network errors, 5xx and 429
|
|
70
|
+
*/
|
|
71
|
+
constructor(message: string, status: number, retryable: boolean);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Ask the Cloud for a pairing.
|
|
75
|
+
*
|
|
76
|
+
* @param cloudUrl - Cloud API base URL (e.g. https://api.crewlyai.com)
|
|
77
|
+
* @param input - How the device introduces itself on the approve page
|
|
78
|
+
* @param fetchImpl - fetch (tests)
|
|
79
|
+
* @returns Device code, user code, link, lifetime and interval
|
|
80
|
+
* @throws DevicePairingError when the Cloud refuses or is unreachable
|
|
81
|
+
*/
|
|
82
|
+
export declare function startCloudDevicePairing(cloudUrl: string, input: {
|
|
83
|
+
deviceName: string;
|
|
84
|
+
deviceId?: string;
|
|
85
|
+
purpose?: string;
|
|
86
|
+
}, fetchImpl?: FetchLike): Promise<DevicePairingStartResult>;
|
|
87
|
+
/**
|
|
88
|
+
* Poll a pairing once.
|
|
89
|
+
*
|
|
90
|
+
* @param cloudUrl - Cloud API base URL
|
|
91
|
+
* @param deviceCode - Secret from {@link startCloudDevicePairing}
|
|
92
|
+
* @param fetchImpl - fetch (tests)
|
|
93
|
+
* @returns The Cloud's answer
|
|
94
|
+
* @throws DevicePairingError when the request fails (see `retryable`)
|
|
95
|
+
*/
|
|
96
|
+
export declare function pollCloudDevicePairing(cloudUrl: string, deviceCode: string, fetchImpl?: FetchLike): Promise<DevicePairingPollResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Wait `ms`, resolving early when `signal` aborts.
|
|
99
|
+
*
|
|
100
|
+
* @param ms - Milliseconds
|
|
101
|
+
* @param signal - Optional abort signal
|
|
102
|
+
* @returns Resolves after the delay or on abort
|
|
103
|
+
*/
|
|
104
|
+
export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
105
|
+
/** Options for {@link waitForCloudDeviceApproval}. */
|
|
106
|
+
export interface WaitForApprovalOptions {
|
|
107
|
+
cloudUrl: string;
|
|
108
|
+
start: DevicePairingStartResult;
|
|
109
|
+
fetchImpl?: FetchLike;
|
|
110
|
+
/** Sleep (tests pass an instant one) */
|
|
111
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
112
|
+
/** Clock (tests) */
|
|
113
|
+
now?: () => number;
|
|
114
|
+
/** Stop waiting (returns `cancelled`) */
|
|
115
|
+
signal?: AbortSignal;
|
|
116
|
+
/** Called after every poll with the status seen (progress display) */
|
|
117
|
+
onPoll?: (status: DevicePairingPollResult['status'], intervalS: number) => void;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Poll until the owner approves or denies, or the pairing expires.
|
|
121
|
+
*
|
|
122
|
+
* Honours the Cloud's interval, adds {@link CLOUD_DEVICE_PAIRING_CONSTANTS.SLOW_DOWN_INCREMENT_S}
|
|
123
|
+
* on `slow_down` / HTTP 429 (capped), and rides out up to
|
|
124
|
+
* {@link CLOUD_DEVICE_PAIRING_CONSTANTS.MAX_CONSECUTIVE_POLL_ERRORS} network
|
|
125
|
+
* or server errors in a row (a laptop changing Wi-Fi mid-login).
|
|
126
|
+
*
|
|
127
|
+
* @param options - Cloud URL, the started pairing and hooks
|
|
128
|
+
* @returns How it ended; `approved` carries the token pair
|
|
129
|
+
* @throws DevicePairingError on a non-retryable refusal or too many errors in a row
|
|
130
|
+
*/
|
|
131
|
+
export declare function waitForCloudDeviceApproval(options: WaitForApprovalOptions): Promise<DevicePairingOutcome>;
|
|
132
|
+
//# sourceMappingURL=cloud-device-pairing.client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-device-pairing.client.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/cloud/cloud-device-pairing.client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud device-code pairing client.
|
|
3
|
+
*
|
|
4
|
+
* Talks to crewly-auth's `/api/cloud/device/*` endpoints so this machine can
|
|
5
|
+
* sign in to the owner's Crewly account without anyone at it:
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link startCloudDevicePairing} — get a secret `deviceCode`, a short
|
|
8
|
+
* `userCode` (`ABCD-2345`) and the link the owner opens
|
|
9
|
+
* (`crewlyai.com/cloud/pair?code=ABCD-2345`).
|
|
10
|
+
* 2. {@link waitForCloudDeviceApproval} — poll at the Cloud's interval
|
|
11
|
+
* (backing off on `slow_down` and rate limits) until the owner approves,
|
|
12
|
+
* denies, or the pairing expires. On approval the Cloud hands over the
|
|
13
|
+
* token pair exactly once.
|
|
14
|
+
*
|
|
15
|
+
* Shared by `crewly cloud login` (CLI) and {@link CloudDevicePairingService}
|
|
16
|
+
* (backend, for `/setup` and Settings → Cloud). No state; the device code
|
|
17
|
+
* never leaves the caller's memory.
|
|
18
|
+
*
|
|
19
|
+
* @module services/cloud/cloud-device-pairing.client
|
|
20
|
+
*/
|
|
21
|
+
import { CLOUD_DEVICE_PAIRING_CONSTANTS } from '../../../../config/constants.js';
|
|
22
|
+
const C = CLOUD_DEVICE_PAIRING_CONSTANTS;
|
|
23
|
+
/** Milliseconds per second. */
|
|
24
|
+
const MS_PER_S = 1000;
|
|
25
|
+
/** HTTP 429. */
|
|
26
|
+
const HTTP_TOO_MANY_REQUESTS = 429;
|
|
27
|
+
/** Lowest HTTP status treated as a server-side (retryable) failure. */
|
|
28
|
+
const HTTP_SERVER_ERROR_MIN = 500;
|
|
29
|
+
/** A request the Cloud refused (or could not answer). */
|
|
30
|
+
export class DevicePairingError extends Error {
|
|
31
|
+
status;
|
|
32
|
+
retryable;
|
|
33
|
+
/**
|
|
34
|
+
* @param message - Human-readable reason (never contains a secret)
|
|
35
|
+
* @param status - HTTP status, 0 when the Cloud was unreachable
|
|
36
|
+
* @param retryable - True for network errors, 5xx and 429
|
|
37
|
+
*/
|
|
38
|
+
constructor(message, status, retryable) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.retryable = retryable;
|
|
42
|
+
this.name = 'DevicePairingError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* POST JSON to crewly-auth and unwrap `{ success, data }`.
|
|
47
|
+
*
|
|
48
|
+
* @param fetchImpl - fetch
|
|
49
|
+
* @param url - Absolute URL
|
|
50
|
+
* @param body - JSON body
|
|
51
|
+
* @returns `data`
|
|
52
|
+
* @throws DevicePairingError on transport errors and non-success replies
|
|
53
|
+
*/
|
|
54
|
+
async function postJson(fetchImpl, url, body) {
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await fetchImpl(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify(body),
|
|
61
|
+
signal: AbortSignal.timeout(C.REQUEST_TIMEOUT_MS),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new DevicePairingError(`Crewly Cloud unreachable: ${err instanceof Error ? err.message : String(err)}`, 0, true);
|
|
66
|
+
}
|
|
67
|
+
let parsed = {};
|
|
68
|
+
try {
|
|
69
|
+
parsed = (await response.json());
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// handled below
|
|
73
|
+
}
|
|
74
|
+
if (!response.ok || !parsed.success || parsed.data === undefined) {
|
|
75
|
+
const retryable = response.status === HTTP_TOO_MANY_REQUESTS || response.status >= HTTP_SERVER_ERROR_MIN;
|
|
76
|
+
throw new DevicePairingError(parsed.error || `Crewly Cloud returned HTTP ${response.status}`, response.status, retryable);
|
|
77
|
+
}
|
|
78
|
+
return parsed.data;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Join the Cloud base URL and an endpoint path.
|
|
82
|
+
*
|
|
83
|
+
* @param cloudUrl - Cloud API base URL
|
|
84
|
+
* @param path - Endpoint path
|
|
85
|
+
* @returns Absolute URL
|
|
86
|
+
*/
|
|
87
|
+
function endpoint(cloudUrl, path) {
|
|
88
|
+
return `${cloudUrl.replace(/\/+$/, '')}${path}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Ask the Cloud for a pairing.
|
|
92
|
+
*
|
|
93
|
+
* @param cloudUrl - Cloud API base URL (e.g. https://api.crewlyai.com)
|
|
94
|
+
* @param input - How the device introduces itself on the approve page
|
|
95
|
+
* @param fetchImpl - fetch (tests)
|
|
96
|
+
* @returns Device code, user code, link, lifetime and interval
|
|
97
|
+
* @throws DevicePairingError when the Cloud refuses or is unreachable
|
|
98
|
+
*/
|
|
99
|
+
export async function startCloudDevicePairing(cloudUrl, input, fetchImpl = fetch) {
|
|
100
|
+
const data = await postJson(fetchImpl, endpoint(cloudUrl, C.CLOUD_ENDPOINTS.START), input);
|
|
101
|
+
if (!data.deviceCode || !data.userCode || !data.verificationUrl) {
|
|
102
|
+
throw new DevicePairingError('Crewly Cloud returned an incomplete pairing', 0, false);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
deviceCode: data.deviceCode,
|
|
106
|
+
userCode: data.userCode,
|
|
107
|
+
verificationUrl: data.verificationUrl,
|
|
108
|
+
...(data.verificationUri ? { verificationUri: data.verificationUri } : {}),
|
|
109
|
+
expiresIn: typeof data.expiresIn === 'number' ? data.expiresIn : C.DEFAULT_EXPIRES_IN_S,
|
|
110
|
+
interval: typeof data.interval === 'number' ? data.interval : C.DEFAULT_INTERVAL_S,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Poll a pairing once.
|
|
115
|
+
*
|
|
116
|
+
* @param cloudUrl - Cloud API base URL
|
|
117
|
+
* @param deviceCode - Secret from {@link startCloudDevicePairing}
|
|
118
|
+
* @param fetchImpl - fetch (tests)
|
|
119
|
+
* @returns The Cloud's answer
|
|
120
|
+
* @throws DevicePairingError when the request fails (see `retryable`)
|
|
121
|
+
*/
|
|
122
|
+
export async function pollCloudDevicePairing(cloudUrl, deviceCode, fetchImpl = fetch) {
|
|
123
|
+
const data = await postJson(fetchImpl, endpoint(cloudUrl, C.CLOUD_ENDPOINTS.POLL), { deviceCode });
|
|
124
|
+
switch (data.status) {
|
|
125
|
+
case C.POLL_STATUS.APPROVED:
|
|
126
|
+
if (!data.token || !data.refreshToken) {
|
|
127
|
+
throw new DevicePairingError('Crewly Cloud approved the device but sent no tokens', 0, false);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
status: 'approved',
|
|
131
|
+
credentials: { token: data.token, refreshToken: data.refreshToken, tier: data.tier || 'free', ...(data.email ? { email: data.email } : {}) },
|
|
132
|
+
};
|
|
133
|
+
case C.POLL_STATUS.PENDING:
|
|
134
|
+
case C.POLL_STATUS.SLOW_DOWN:
|
|
135
|
+
return { status: data.status, ...(typeof data.interval === 'number' ? { interval: data.interval } : {}) };
|
|
136
|
+
case C.POLL_STATUS.DENIED:
|
|
137
|
+
return { status: 'denied' };
|
|
138
|
+
case C.POLL_STATUS.EXPIRED:
|
|
139
|
+
return { status: 'expired' };
|
|
140
|
+
default:
|
|
141
|
+
throw new DevicePairingError(`Unexpected pairing status: ${String(data.status)}`, 0, false);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Wait `ms`, resolving early when `signal` aborts.
|
|
146
|
+
*
|
|
147
|
+
* @param ms - Milliseconds
|
|
148
|
+
* @param signal - Optional abort signal
|
|
149
|
+
* @returns Resolves after the delay or on abort
|
|
150
|
+
*/
|
|
151
|
+
export function abortableSleep(ms, signal) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
if (signal?.aborted) {
|
|
154
|
+
resolve();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
signal?.removeEventListener('abort', onAbort);
|
|
159
|
+
resolve();
|
|
160
|
+
}, ms);
|
|
161
|
+
const onAbort = () => {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
resolve();
|
|
164
|
+
};
|
|
165
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Poll until the owner approves or denies, or the pairing expires.
|
|
170
|
+
*
|
|
171
|
+
* Honours the Cloud's interval, adds {@link CLOUD_DEVICE_PAIRING_CONSTANTS.SLOW_DOWN_INCREMENT_S}
|
|
172
|
+
* on `slow_down` / HTTP 429 (capped), and rides out up to
|
|
173
|
+
* {@link CLOUD_DEVICE_PAIRING_CONSTANTS.MAX_CONSECUTIVE_POLL_ERRORS} network
|
|
174
|
+
* or server errors in a row (a laptop changing Wi-Fi mid-login).
|
|
175
|
+
*
|
|
176
|
+
* @param options - Cloud URL, the started pairing and hooks
|
|
177
|
+
* @returns How it ended; `approved` carries the token pair
|
|
178
|
+
* @throws DevicePairingError on a non-retryable refusal or too many errors in a row
|
|
179
|
+
*/
|
|
180
|
+
export async function waitForCloudDeviceApproval(options) {
|
|
181
|
+
const { cloudUrl, start, signal, onPoll } = options;
|
|
182
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
183
|
+
const sleep = options.sleep ?? abortableSleep;
|
|
184
|
+
const now = options.now ?? (() => Date.now());
|
|
185
|
+
const deadline = now() + start.expiresIn * MS_PER_S;
|
|
186
|
+
let intervalS = Math.max(1, start.interval || C.DEFAULT_INTERVAL_S);
|
|
187
|
+
let consecutiveErrors = 0;
|
|
188
|
+
for (;;) {
|
|
189
|
+
await sleep(intervalS * MS_PER_S, signal);
|
|
190
|
+
if (signal?.aborted)
|
|
191
|
+
return { status: 'cancelled' };
|
|
192
|
+
if (now() >= deadline)
|
|
193
|
+
return { status: 'expired' };
|
|
194
|
+
let result;
|
|
195
|
+
try {
|
|
196
|
+
result = await pollCloudDevicePairing(cloudUrl, start.deviceCode, fetchImpl);
|
|
197
|
+
consecutiveErrors = 0;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (!(err instanceof DevicePairingError) || !err.retryable)
|
|
201
|
+
throw err;
|
|
202
|
+
consecutiveErrors++;
|
|
203
|
+
if (consecutiveErrors > C.MAX_CONSECUTIVE_POLL_ERRORS)
|
|
204
|
+
throw err;
|
|
205
|
+
if (err.status === HTTP_TOO_MANY_REQUESTS) {
|
|
206
|
+
intervalS = Math.min(C.MAX_INTERVAL_S, intervalS + C.SLOW_DOWN_INCREMENT_S);
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (signal?.aborted)
|
|
211
|
+
return { status: 'cancelled' };
|
|
212
|
+
onPoll?.(result.status, intervalS);
|
|
213
|
+
switch (result.status) {
|
|
214
|
+
case 'approved':
|
|
215
|
+
return result;
|
|
216
|
+
case 'expired':
|
|
217
|
+
case 'denied':
|
|
218
|
+
return { status: result.status };
|
|
219
|
+
case 'slow_down':
|
|
220
|
+
intervalS = Math.min(C.MAX_INTERVAL_S, result.interval ?? intervalS + C.SLOW_DOWN_INCREMENT_S);
|
|
221
|
+
break;
|
|
222
|
+
default:
|
|
223
|
+
if (typeof result.interval === 'number')
|
|
224
|
+
intervalS = Math.min(C.MAX_INTERVAL_S, Math.max(1, result.interval));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=cloud-device-pairing.client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-device-pairing.client.js","sourceRoot":"","sources":["../../../../../../backend/src/services/cloud/cloud-device-pairing.client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud device pairing (backend side).
|
|
3
|
+
*
|
|
4
|
+
* Lets the web UI (`/setup`'s Cloud step, Settings → Cloud) — often opened on
|
|
5
|
+
* the owner's phone over the LAN or the relay — connect this machine to
|
|
6
|
+
* Crewly Cloud with no token handling at all:
|
|
7
|
+
*
|
|
8
|
+
* 1. `start()` asks crewly-auth for a pairing and returns the link + short
|
|
9
|
+
* code to show (as a QR code too).
|
|
10
|
+
* 2. The service polls crewly-auth in the background. When the owner taps
|
|
11
|
+
* Approve on crewlyai.com/cloud/pair, it receives the token pair and
|
|
12
|
+
* connects by itself through the same path as `POST /api/cloud/connect`
|
|
13
|
+
* (injected as `connect`).
|
|
14
|
+
* 3. The UI polls `status()` until it reads `connected`.
|
|
15
|
+
*
|
|
16
|
+
* One pairing at a time; `start()` while one is pending returns the same
|
|
17
|
+
* code, so reloading the page does not spawn new codes. The device code and
|
|
18
|
+
* the tokens never appear in `status()`.
|
|
19
|
+
*
|
|
20
|
+
* @module services/cloud/cloud-device-pairing.service
|
|
21
|
+
*/
|
|
22
|
+
import { type ComponentLogger } from '../core/logger.service.js';
|
|
23
|
+
import { startCloudDevicePairing, type DevicePairingCredentials, type DevicePairingOutcome, type WaitForApprovalOptions } from './cloud-device-pairing.client.js';
|
|
24
|
+
/** Where a pairing stands. */
|
|
25
|
+
export type CloudDevicePairingState = 'idle' | 'pending' | 'connected' | 'expired' | 'denied' | 'cancelled' | 'error';
|
|
26
|
+
/** What the UI sees (no secrets). */
|
|
27
|
+
export interface CloudDevicePairingStatus {
|
|
28
|
+
state: CloudDevicePairingState;
|
|
29
|
+
/** Code the owner checks on the approve page */
|
|
30
|
+
userCode?: string;
|
|
31
|
+
/** Approve page with the code (show + QR) */
|
|
32
|
+
verificationUrl?: string;
|
|
33
|
+
/** Approve page without the code */
|
|
34
|
+
verificationUri?: string;
|
|
35
|
+
/** ISO expiry of the pending pairing */
|
|
36
|
+
expiresAt?: string;
|
|
37
|
+
/** Name shown to the owner on the approve page */
|
|
38
|
+
deviceName?: string;
|
|
39
|
+
/** Plan, once connected */
|
|
40
|
+
tier?: string;
|
|
41
|
+
/** Account, once connected */
|
|
42
|
+
email?: string;
|
|
43
|
+
/** Why it failed (`error`) */
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Dependencies (injectable for tests). */
|
|
47
|
+
export interface CloudDevicePairingDeps {
|
|
48
|
+
/** Cloud API base URL */
|
|
49
|
+
cloudUrl: () => string;
|
|
50
|
+
/** How this machine introduces itself */
|
|
51
|
+
identity: () => Promise<{
|
|
52
|
+
deviceId: string;
|
|
53
|
+
deviceName: string;
|
|
54
|
+
}>;
|
|
55
|
+
/** Connect with the approved token pair (performCloudConnect) */
|
|
56
|
+
connect: (credentials: DevicePairingCredentials) => Promise<{
|
|
57
|
+
tier: string;
|
|
58
|
+
}>;
|
|
59
|
+
start?: typeof startCloudDevicePairing;
|
|
60
|
+
wait?: (options: WaitForApprovalOptions) => Promise<DevicePairingOutcome>;
|
|
61
|
+
now?: () => number;
|
|
62
|
+
logger?: ComponentLogger;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Runs one device pairing at a time in the background.
|
|
66
|
+
*/
|
|
67
|
+
export declare class CloudDevicePairingService {
|
|
68
|
+
private readonly deps;
|
|
69
|
+
private readonly logger;
|
|
70
|
+
private status;
|
|
71
|
+
private controller;
|
|
72
|
+
/** Settles when the current background run ends (tests await it). */
|
|
73
|
+
private running;
|
|
74
|
+
/**
|
|
75
|
+
* @param deps - Cloud URL, identity, connect and the pairing client
|
|
76
|
+
*/
|
|
77
|
+
constructor(deps: CloudDevicePairingDeps);
|
|
78
|
+
/**
|
|
79
|
+
* Current status (never contains the device code or tokens).
|
|
80
|
+
*
|
|
81
|
+
* @returns A copy of the status
|
|
82
|
+
*/
|
|
83
|
+
getStatus(): CloudDevicePairingStatus;
|
|
84
|
+
/**
|
|
85
|
+
* Start a pairing, or return the one already pending.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Optional device name override (defaults to the hostname)
|
|
88
|
+
* @returns Status with the link and code to show
|
|
89
|
+
* @throws Error when crewly-auth refuses or cannot be reached
|
|
90
|
+
*/
|
|
91
|
+
start(options?: {
|
|
92
|
+
deviceName?: string;
|
|
93
|
+
}): Promise<CloudDevicePairingStatus>;
|
|
94
|
+
/**
|
|
95
|
+
* Stop waiting for the current pairing.
|
|
96
|
+
*
|
|
97
|
+
* @returns The status after cancelling
|
|
98
|
+
*/
|
|
99
|
+
cancel(): CloudDevicePairingStatus;
|
|
100
|
+
/**
|
|
101
|
+
* Wait for the current background run to end (tests).
|
|
102
|
+
*
|
|
103
|
+
* @returns Resolves when no run is in flight
|
|
104
|
+
*/
|
|
105
|
+
settled(): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Background: wait for the owner, then connect.
|
|
108
|
+
*
|
|
109
|
+
* @param cloudUrl - Cloud API base URL
|
|
110
|
+
* @param started - The pairing
|
|
111
|
+
* @param controller - Aborts this run
|
|
112
|
+
*/
|
|
113
|
+
private run;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=cloud-device-pairing.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-device-pairing.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/cloud/cloud-device-pairing.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,EACL,uBAAuB,EAEvB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EAEzB,KAAK,sBAAsB,EAC5B,MAAM,kCAAkC,CAAC;AAK1C,8BAA8B;AAC9B,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAEtH,qCAAqC;AACrC,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,uBAAuB,CAAC;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oCAAoC;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB;IACrC,yBAAyB;IACzB,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,iEAAiE;IACjE,OAAO,EAAE,CAAC,WAAW,EAAE,wBAAwB,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,OAAO,uBAAuB,CAAC;IACvC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC1E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;GAEG;AACH,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmD;IACxE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,MAAM,CAA+C;IAC7D,OAAO,CAAC,UAAU,CAAgC;IAClD,qEAAqE;IACrE,OAAO,CAAC,OAAO,CAA8B;IAE7C;;OAEG;gBACS,IAAI,EAAE,sBAAsB;IAYxC;;;;OAIG;IACH,SAAS,IAAI,wBAAwB;IAOrC;;;;;;OAMG;IACG,KAAK,CAAC,OAAO,GAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,wBAAwB,CAAC;IA6BrF;;;;OAIG;IACH,MAAM,IAAI,wBAAwB;IAUlC;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;;;;;OAMG;YACW,GAAG;CA4BlB"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud device pairing (backend side).
|
|
3
|
+
*
|
|
4
|
+
* Lets the web UI (`/setup`'s Cloud step, Settings → Cloud) — often opened on
|
|
5
|
+
* the owner's phone over the LAN or the relay — connect this machine to
|
|
6
|
+
* Crewly Cloud with no token handling at all:
|
|
7
|
+
*
|
|
8
|
+
* 1. `start()` asks crewly-auth for a pairing and returns the link + short
|
|
9
|
+
* code to show (as a QR code too).
|
|
10
|
+
* 2. The service polls crewly-auth in the background. When the owner taps
|
|
11
|
+
* Approve on crewlyai.com/cloud/pair, it receives the token pair and
|
|
12
|
+
* connects by itself through the same path as `POST /api/cloud/connect`
|
|
13
|
+
* (injected as `connect`).
|
|
14
|
+
* 3. The UI polls `status()` until it reads `connected`.
|
|
15
|
+
*
|
|
16
|
+
* One pairing at a time; `start()` while one is pending returns the same
|
|
17
|
+
* code, so reloading the page does not spawn new codes. The device code and
|
|
18
|
+
* the tokens never appear in `status()`.
|
|
19
|
+
*
|
|
20
|
+
* @module services/cloud/cloud-device-pairing.service
|
|
21
|
+
*/
|
|
22
|
+
import { CLOUD_DEVICE_PAIRING_CONSTANTS } from '../../../../config/constants.js';
|
|
23
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
24
|
+
import { startCloudDevicePairing, waitForCloudDeviceApproval, } from './cloud-device-pairing.client.js';
|
|
25
|
+
/** Milliseconds per second. */
|
|
26
|
+
const MS_PER_S = 1000;
|
|
27
|
+
/**
|
|
28
|
+
* Runs one device pairing at a time in the background.
|
|
29
|
+
*/
|
|
30
|
+
export class CloudDevicePairingService {
|
|
31
|
+
deps;
|
|
32
|
+
logger;
|
|
33
|
+
status = { state: 'idle' };
|
|
34
|
+
controller = null;
|
|
35
|
+
/** Settles when the current background run ends (tests await it). */
|
|
36
|
+
running = null;
|
|
37
|
+
/**
|
|
38
|
+
* @param deps - Cloud URL, identity, connect and the pairing client
|
|
39
|
+
*/
|
|
40
|
+
constructor(deps) {
|
|
41
|
+
this.deps = {
|
|
42
|
+
cloudUrl: deps.cloudUrl,
|
|
43
|
+
identity: deps.identity,
|
|
44
|
+
connect: deps.connect,
|
|
45
|
+
start: deps.start ?? startCloudDevicePairing,
|
|
46
|
+
wait: deps.wait ?? waitForCloudDeviceApproval,
|
|
47
|
+
now: deps.now ?? (() => Date.now()),
|
|
48
|
+
};
|
|
49
|
+
this.logger = deps.logger ?? LoggerService.getInstance().createComponentLogger('CloudDevicePairing');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Current status (never contains the device code or tokens).
|
|
53
|
+
*
|
|
54
|
+
* @returns A copy of the status
|
|
55
|
+
*/
|
|
56
|
+
getStatus() {
|
|
57
|
+
if (this.status.state === 'pending' && this.status.expiresAt && Date.parse(this.status.expiresAt) <= this.deps.now()) {
|
|
58
|
+
return { ...this.status, state: 'expired' };
|
|
59
|
+
}
|
|
60
|
+
return { ...this.status };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Start a pairing, or return the one already pending.
|
|
64
|
+
*
|
|
65
|
+
* @param options - Optional device name override (defaults to the hostname)
|
|
66
|
+
* @returns Status with the link and code to show
|
|
67
|
+
* @throws Error when crewly-auth refuses or cannot be reached
|
|
68
|
+
*/
|
|
69
|
+
async start(options = {}) {
|
|
70
|
+
const current = this.getStatus();
|
|
71
|
+
if (current.state === 'pending')
|
|
72
|
+
return current;
|
|
73
|
+
const identity = await this.deps.identity();
|
|
74
|
+
const deviceName = options.deviceName?.trim() || identity.deviceName;
|
|
75
|
+
const cloudUrl = this.deps.cloudUrl();
|
|
76
|
+
const started = await this.deps.start(cloudUrl, {
|
|
77
|
+
deviceName,
|
|
78
|
+
deviceId: identity.deviceId,
|
|
79
|
+
purpose: CLOUD_DEVICE_PAIRING_CONSTANTS.PURPOSES.SETUP,
|
|
80
|
+
});
|
|
81
|
+
this.controller?.abort();
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
this.controller = controller;
|
|
84
|
+
this.status = {
|
|
85
|
+
state: 'pending',
|
|
86
|
+
userCode: started.userCode,
|
|
87
|
+
verificationUrl: started.verificationUrl,
|
|
88
|
+
...(started.verificationUri ? { verificationUri: started.verificationUri } : {}),
|
|
89
|
+
expiresAt: new Date(this.deps.now() + started.expiresIn * MS_PER_S).toISOString(),
|
|
90
|
+
deviceName,
|
|
91
|
+
};
|
|
92
|
+
this.logger.info('Cloud device pairing started', { userCode: started.userCode, deviceName });
|
|
93
|
+
this.running = this.run(cloudUrl, started, controller);
|
|
94
|
+
return this.getStatus();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Stop waiting for the current pairing.
|
|
98
|
+
*
|
|
99
|
+
* @returns The status after cancelling
|
|
100
|
+
*/
|
|
101
|
+
cancel() {
|
|
102
|
+
if (this.status.state === 'pending') {
|
|
103
|
+
this.controller?.abort();
|
|
104
|
+
this.controller = null;
|
|
105
|
+
this.status = { state: 'cancelled' };
|
|
106
|
+
this.logger.info('Cloud device pairing cancelled');
|
|
107
|
+
}
|
|
108
|
+
return this.getStatus();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Wait for the current background run to end (tests).
|
|
112
|
+
*
|
|
113
|
+
* @returns Resolves when no run is in flight
|
|
114
|
+
*/
|
|
115
|
+
async settled() {
|
|
116
|
+
await this.running;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Background: wait for the owner, then connect.
|
|
120
|
+
*
|
|
121
|
+
* @param cloudUrl - Cloud API base URL
|
|
122
|
+
* @param started - The pairing
|
|
123
|
+
* @param controller - Aborts this run
|
|
124
|
+
*/
|
|
125
|
+
async run(cloudUrl, started, controller) {
|
|
126
|
+
const isCurrent = () => this.controller === controller && !controller.signal.aborted;
|
|
127
|
+
try {
|
|
128
|
+
const outcome = await this.deps.wait({ cloudUrl, start: started, signal: controller.signal });
|
|
129
|
+
if (!isCurrent())
|
|
130
|
+
return;
|
|
131
|
+
if (outcome.status !== 'approved') {
|
|
132
|
+
this.status = { ...this.status, state: outcome.status };
|
|
133
|
+
this.logger.info('Cloud device pairing ended', { state: outcome.status });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const { tier } = await this.deps.connect(outcome.credentials);
|
|
137
|
+
if (!isCurrent())
|
|
138
|
+
return;
|
|
139
|
+
this.status = {
|
|
140
|
+
state: 'connected',
|
|
141
|
+
deviceName: this.status.deviceName,
|
|
142
|
+
tier,
|
|
143
|
+
...(outcome.credentials.email ? { email: outcome.credentials.email } : {}),
|
|
144
|
+
};
|
|
145
|
+
this.logger.info('Connected to Crewly Cloud via device pairing', { tier });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (!isCurrent())
|
|
149
|
+
return;
|
|
150
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
151
|
+
this.status = { state: 'error', error: message, deviceName: this.status.deviceName };
|
|
152
|
+
this.logger.warn('Cloud device pairing failed', { error: message });
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (this.controller === controller)
|
|
156
|
+
this.controller = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=cloud-device-pairing.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-device-pairing.service.js","sourceRoot":"","sources":["../../../../../../backend/src/services/cloud/cloud-device-pairing.service.ts"],"names":[],"mappings":"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"}
|