gaoding-cli 1.0.0-alpha.10
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/LICENSE +21 -0
- package/README.md +38 -0
- package/contracts/operations/agent.send/input.schema.json +354 -0
- package/contracts/operations/agent.send/output.schema.json +271 -0
- package/contracts/operations/auth.status/input.schema.json +7 -0
- package/contracts/operations/auth.status/output.schema.json +132 -0
- package/contracts/operations/dam.delete/input.schema.json +23 -0
- package/contracts/operations/dam.delete/output.schema.json +6 -0
- package/contracts/operations/dam.get/input.schema.json +16 -0
- package/contracts/operations/dam.get/output.schema.json +46 -0
- package/contracts/operations/dam.list/input.schema.json +44 -0
- package/contracts/operations/dam.list/output.schema.json +71 -0
- package/contracts/operations/dam.search/input.schema.json +46 -0
- package/contracts/operations/dam.search/output.schema.json +71 -0
- package/contracts/operations/dam.upload/input.schema.json +31 -0
- package/contracts/operations/dam.upload/output.schema.json +46 -0
- package/contracts/operations/editor.apply/input.schema.json +134 -0
- package/contracts/operations/editor.apply/output.schema.json +15 -0
- package/contracts/operations/editor.connect/input.schema.json +15 -0
- package/contracts/operations/editor.connect/output.schema.json +17 -0
- package/contracts/operations/editor.disconnect/input.schema.json +8 -0
- package/contracts/operations/editor.disconnect/output.schema.json +14 -0
- package/contracts/operations/editor.save/input.schema.json +8 -0
- package/contracts/operations/editor.save/output.schema.json +23 -0
- package/contracts/operations/editor.screenshot/input.schema.json +8 -0
- package/contracts/operations/editor.screenshot/output.schema.json +16 -0
- package/contracts/operations/editor.snapshot/input.schema.json +8 -0
- package/contracts/operations/editor.snapshot/output.schema.json +113 -0
- package/contracts/operations/model.get/input.schema.json +18 -0
- package/contracts/operations/model.get/output.schema.json +190 -0
- package/contracts/operations/model.list/input.schema.json +15 -0
- package/contracts/operations/model.list/output.schema.json +111 -0
- package/contracts/operations/org.current/input.schema.json +7 -0
- package/contracts/operations/org.current/output.schema.json +49 -0
- package/contracts/operations/org.list/input.schema.json +7 -0
- package/contracts/operations/org.list/output.schema.json +71 -0
- package/contracts/operations/tool.call/input.schema.json +35 -0
- package/contracts/operations/tool.call/output.schema.json +129 -0
- package/contracts/operations/tool.list/input.schema.json +7 -0
- package/contracts/operations/tool.list/output.schema.json +45 -0
- package/dist/bin/gd-cli.js +38 -0
- package/dist/bin/postinstall.js +42 -0
- package/dist/src/bootstrap/create-cli.js +55 -0
- package/dist/src/bootstrap/create-runtime.js +222 -0
- package/dist/src/bootstrap/validators.js +118 -0
- package/dist/src/cli/action-binding.js +63 -0
- package/dist/src/cli/agent-commands.js +39 -0
- package/dist/src/cli/auth-commands.js +47 -0
- package/dist/src/cli/dam-commands.js +207 -0
- package/dist/src/cli/editor-commands.js +73 -0
- package/dist/src/cli/errors.js +90 -0
- package/dist/src/cli/model-commands.js +47 -0
- package/dist/src/cli/org-commands.js +52 -0
- package/dist/src/cli/presenter.js +80 -0
- package/dist/src/cli/prompt.js +30 -0
- package/dist/src/cli/tool-commands.js +65 -0
- package/dist/src/cli/update-command.js +12 -0
- package/dist/src/contracts/schema.js +4 -0
- package/dist/src/features/agent/creative-agent-adapter.js +31 -0
- package/dist/src/features/agent/creative-protocol.js +324 -0
- package/dist/src/features/agent/creative-stream.js +127 -0
- package/dist/src/features/agent/use-cases.js +113 -0
- package/dist/src/features/auth/access-policy.js +101 -0
- package/dist/src/features/auth/credential-store.js +62 -0
- package/dist/src/features/auth/sso-service.js +179 -0
- package/dist/src/features/auth/state.js +129 -0
- package/dist/src/features/auth/use-cases.js +106 -0
- package/dist/src/features/dam/asset-projection.js +270 -0
- package/dist/src/features/dam/dam-api-adapter.js +206 -0
- package/dist/src/features/dam/object-storage.js +191 -0
- package/dist/src/features/dam/registered-uploader.js +224 -0
- package/dist/src/features/dam/storage-upload.js +141 -0
- package/dist/src/features/dam/transient-uploader.js +17 -0
- package/dist/src/features/dam/use-cases.js +151 -0
- package/dist/src/features/editor/bridge-client.js +96 -0
- package/dist/src/features/editor/bridge-process.js +222 -0
- package/dist/src/features/editor/bridge-server.js +311 -0
- package/dist/src/features/editor/protocol.js +1 -0
- package/dist/src/features/editor/session-state.js +78 -0
- package/dist/src/features/editor/session.js +193 -0
- package/dist/src/features/editor/use-cases.js +61 -0
- package/dist/src/features/org/org-service.js +76 -0
- package/dist/src/features/org/use-cases.js +140 -0
- package/dist/src/features/skill/bundled-skills.js +55 -0
- package/dist/src/features/skill/installer.js +265 -0
- package/dist/src/features/tool/catalog.js +79 -0
- package/dist/src/features/tool/dynamic-schema.js +96 -0
- package/dist/src/features/tool/mns-catalog-adapter.js +262 -0
- package/dist/src/features/tool/tool-api-adapter.js +200 -0
- package/dist/src/features/tool/use-cases.js +126 -0
- package/dist/src/features/update/update-service.js +194 -0
- package/dist/src/platform/json-input.js +64 -0
- package/dist/src/platform/local-json-file.js +59 -0
- package/dist/src/platform/open-browser.js +8 -0
- package/dist/src/platform/redact.js +80 -0
- package/dist/src/platform/safe-upload-file.js +146 -0
- package/dist/src/platform/signature.js +19 -0
- package/dist/src/platform/signed-http-transport.js +109 -0
- package/dist/src/platform/url-safety.js +100 -0
- package/package.json +56 -0
- package/skills/gd-cli/SKILL.md +15 -0
- package/skills/gd-cli/references/auth-org.md +9 -0
- package/skills/gd-cli/references/creation.md +38 -0
- package/skills/gd-cli/references/dam.md +20 -0
- package/skills/gd-cli/references/editor.md +12 -0
- package/skills/gd-cli/references/errors.md +10 -0
- package/skills/gd-cli/references/update.md +11 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createLocalJsonFile, LocalFileError } from "../../platform/local-json-file.js";
|
|
4
|
+
import { AuthStateInvalidError, parseStoredAuthState } from "./state.js";
|
|
5
|
+
export function createLocalCredentialStore(options = {}) {
|
|
6
|
+
const file = options.file ?? createLocalJsonFile({
|
|
7
|
+
directory: join(options.homeDirectory ?? homedir(), ".gd"),
|
|
8
|
+
filename: "credentials.json"
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
async read() {
|
|
12
|
+
try {
|
|
13
|
+
const text = await file.readText();
|
|
14
|
+
if (text === null)
|
|
15
|
+
return null;
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
catch (cause) {
|
|
21
|
+
throw new AuthStateInvalidError({ cause });
|
|
22
|
+
}
|
|
23
|
+
const parsed = parseStoredAuthState(raw);
|
|
24
|
+
if (parsed.migrated)
|
|
25
|
+
await file.writeTextAtomically(serialize(parsed.state));
|
|
26
|
+
return parsed.state;
|
|
27
|
+
}
|
|
28
|
+
catch (cause) {
|
|
29
|
+
if (cause instanceof AuthStateInvalidError)
|
|
30
|
+
throw cause;
|
|
31
|
+
throw invalidState(cause);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async write(state) {
|
|
35
|
+
try {
|
|
36
|
+
const validated = parseStoredAuthState(state).state;
|
|
37
|
+
await file.writeTextAtomically(serialize(validated));
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
if (cause instanceof AuthStateInvalidError)
|
|
41
|
+
throw cause;
|
|
42
|
+
throw invalidState(cause);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async remove() {
|
|
46
|
+
try {
|
|
47
|
+
await file.remove();
|
|
48
|
+
}
|
|
49
|
+
catch (cause) {
|
|
50
|
+
throw invalidState(cause);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function serialize(state) {
|
|
56
|
+
return `${JSON.stringify(state, null, 2)}\n`;
|
|
57
|
+
}
|
|
58
|
+
function invalidState(cause) {
|
|
59
|
+
return new AuthStateInvalidError({
|
|
60
|
+
cause: cause instanceof LocalFileError ? cause.cause : cause
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { normalizeScope } from "./state.js";
|
|
2
|
+
import { signBindOrganization } from "../../platform/signature.js";
|
|
3
|
+
export class SsoRequestError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
constructor(status) {
|
|
6
|
+
super(status === undefined ? "无法连接稿定登录服务。" : `稿定登录服务请求失败(HTTP ${status})。`);
|
|
7
|
+
this.name = "SsoRequestError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function classifyBindOutcome(input) {
|
|
12
|
+
const code = input.code === undefined ? "" : String(input.code).trim().toUpperCase();
|
|
13
|
+
const details = input.message ? { message: input.message } : {};
|
|
14
|
+
if (!code || code === "0" || code === "200" || code === "OK" || code === "SUCCESS") {
|
|
15
|
+
return input.bindExpiresAt
|
|
16
|
+
? { status: "success", bindExpiresAt: input.bindExpiresAt }
|
|
17
|
+
: { status: "success" };
|
|
18
|
+
}
|
|
19
|
+
return code === "ORG_INVALID"
|
|
20
|
+
? { status: "invalid", ...details }
|
|
21
|
+
: { status: "failed", ...details };
|
|
22
|
+
}
|
|
23
|
+
export function createSsoService(options) {
|
|
24
|
+
const baseUrl = new URL(options.ssoApi);
|
|
25
|
+
if (!baseUrl.pathname.endsWith("/"))
|
|
26
|
+
baseUrl.pathname += "/";
|
|
27
|
+
async function request(relativeUrl, signal) {
|
|
28
|
+
signal.throwIfAborted();
|
|
29
|
+
try {
|
|
30
|
+
return await options.fetch(new URL(relativeUrl, baseUrl), {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { Accept: "application/json" },
|
|
33
|
+
signal: AbortSignal.any([
|
|
34
|
+
signal,
|
|
35
|
+
AbortSignal.timeout(options.timeoutMs ?? 30_000)
|
|
36
|
+
])
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
if (signal.aborted)
|
|
41
|
+
throw signal.reason;
|
|
42
|
+
throw new SsoRequestError();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
async startDeviceAuthorization(signal) {
|
|
47
|
+
const response = await request("oauth/device-authorization?clientId=gdhub-cli", signal);
|
|
48
|
+
if (!response.ok)
|
|
49
|
+
throw new SsoRequestError(response.status);
|
|
50
|
+
const body = await responseRecord(response);
|
|
51
|
+
const authorizeCode = nonEmptyString(body.authorize_code);
|
|
52
|
+
const deviceCode = nonEmptyString(body.device_code);
|
|
53
|
+
const expiresInSeconds = positiveNumber(body.expires_in);
|
|
54
|
+
const intervalSeconds = positiveNumber(body.interval);
|
|
55
|
+
if (!authorizeCode || !deviceCode || !expiresInSeconds || !intervalSeconds) {
|
|
56
|
+
throw new SsoRequestError(response.status);
|
|
57
|
+
}
|
|
58
|
+
return { authorizeCode, deviceCode, expiresInSeconds, intervalSeconds };
|
|
59
|
+
},
|
|
60
|
+
async pollDeviceToken(deviceCode, signal) {
|
|
61
|
+
const query = new URLSearchParams({
|
|
62
|
+
grantType: "device_code",
|
|
63
|
+
deviceCode
|
|
64
|
+
});
|
|
65
|
+
const response = await request(`oauth/device/token?${query}`, signal);
|
|
66
|
+
const body = await responseRecord(response);
|
|
67
|
+
const error = nonEmptyString(body.error)?.toLowerCase();
|
|
68
|
+
if (error === "authorization_pending")
|
|
69
|
+
return { status: "pending" };
|
|
70
|
+
if (error === "slow_down")
|
|
71
|
+
return { status: "slow_down" };
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
throw new SsoRequestError(response.status);
|
|
74
|
+
const accessKey = nonEmptyString(body.ak);
|
|
75
|
+
const secretKey = nonEmptyString(body.sk);
|
|
76
|
+
const expiresAt = dateString(body.expires_at);
|
|
77
|
+
if (!accessKey || !secretKey || !expiresAt)
|
|
78
|
+
throw new SsoRequestError(response.status);
|
|
79
|
+
let scope;
|
|
80
|
+
try {
|
|
81
|
+
scope = normalizeScope(body.scope);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new SsoRequestError(response.status);
|
|
85
|
+
}
|
|
86
|
+
const result = {
|
|
87
|
+
status: "authorized",
|
|
88
|
+
credential: { accessKey, secretKey, expiresAt, scope }
|
|
89
|
+
};
|
|
90
|
+
if (isRecord(body.user)) {
|
|
91
|
+
const id = identifier(body.user.id);
|
|
92
|
+
const name = nonEmptyString(body.user.name);
|
|
93
|
+
if (id && name)
|
|
94
|
+
result.account = { id, name };
|
|
95
|
+
}
|
|
96
|
+
const organizationId = identifier(body.orgId ?? body.org_id ?? body.orgID);
|
|
97
|
+
if (organizationId)
|
|
98
|
+
result.organizationId = organizationId;
|
|
99
|
+
return result;
|
|
100
|
+
},
|
|
101
|
+
async bindOrganization(input, signal) {
|
|
102
|
+
const timestamp = Math.floor(options.now().getTime() / 1000);
|
|
103
|
+
const query = new URLSearchParams({
|
|
104
|
+
ak: input.credential.accessKey,
|
|
105
|
+
orgId: input.organizationId,
|
|
106
|
+
signature: signBindOrganization({
|
|
107
|
+
accessKey: input.credential.accessKey,
|
|
108
|
+
secretKey: input.credential.secretKey,
|
|
109
|
+
organizationId: input.organizationId,
|
|
110
|
+
timestamp
|
|
111
|
+
}),
|
|
112
|
+
timestamp: String(timestamp)
|
|
113
|
+
});
|
|
114
|
+
const response = await request(`oauth/device/bind-org?${query}`, signal);
|
|
115
|
+
if (!response.ok)
|
|
116
|
+
throw new SsoRequestError(response.status);
|
|
117
|
+
const text = await response.text();
|
|
118
|
+
if (!text.trim())
|
|
119
|
+
return {};
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(text);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { message: text.trim() };
|
|
126
|
+
}
|
|
127
|
+
if (!isRecord(parsed))
|
|
128
|
+
throw new SsoRequestError(response.status);
|
|
129
|
+
const result = {};
|
|
130
|
+
if (typeof parsed.code === "string" || typeof parsed.code === "number") {
|
|
131
|
+
result.code = parsed.code;
|
|
132
|
+
}
|
|
133
|
+
const message = nonEmptyString(parsed.message);
|
|
134
|
+
if (message)
|
|
135
|
+
result.message = message;
|
|
136
|
+
if (parsed.bind_expires_at !== undefined) {
|
|
137
|
+
const bindExpiresAt = dateString(parsed.bind_expires_at);
|
|
138
|
+
if (!bindExpiresAt)
|
|
139
|
+
throw new SsoRequestError(response.status);
|
|
140
|
+
result.bindExpiresAt = bindExpiresAt;
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function responseRecord(response) {
|
|
147
|
+
try {
|
|
148
|
+
const value = await response.json();
|
|
149
|
+
if (isRecord(value))
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Handled as one safe protocol failure below.
|
|
154
|
+
}
|
|
155
|
+
throw new SsoRequestError(response.status);
|
|
156
|
+
}
|
|
157
|
+
function positiveNumber(value) {
|
|
158
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
159
|
+
? value
|
|
160
|
+
: undefined;
|
|
161
|
+
}
|
|
162
|
+
function dateString(value) {
|
|
163
|
+
const text = nonEmptyString(value);
|
|
164
|
+
return text && !Number.isNaN(Date.parse(text)) ? text : undefined;
|
|
165
|
+
}
|
|
166
|
+
function identifier(value) {
|
|
167
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
168
|
+
return String(value);
|
|
169
|
+
return nonEmptyString(value);
|
|
170
|
+
}
|
|
171
|
+
function nonEmptyString(value) {
|
|
172
|
+
if (typeof value !== "string")
|
|
173
|
+
return undefined;
|
|
174
|
+
const text = value.trim();
|
|
175
|
+
return text || undefined;
|
|
176
|
+
}
|
|
177
|
+
function isRecord(value) {
|
|
178
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
179
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export class AuthStateInvalidError extends Error {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
super("本地登录状态无效。", options);
|
|
4
|
+
this.name = "AuthStateInvalidError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function parseStoredAuthState(value) {
|
|
8
|
+
try {
|
|
9
|
+
if (!isRecord(value))
|
|
10
|
+
throw new Error("invalid root");
|
|
11
|
+
if (value.version === 1)
|
|
12
|
+
return { state: parseVersionOne(value), migrated: false };
|
|
13
|
+
if (value.version === 2)
|
|
14
|
+
return { state: migrateVersionTwo(value), migrated: true };
|
|
15
|
+
throw new Error("unsupported version");
|
|
16
|
+
}
|
|
17
|
+
catch (cause) {
|
|
18
|
+
if (cause instanceof AuthStateInvalidError)
|
|
19
|
+
throw cause;
|
|
20
|
+
throw new AuthStateInvalidError({ cause });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function parseVersionOne(value) {
|
|
24
|
+
if (!isRecord(value.credential))
|
|
25
|
+
throw new Error("missing credential");
|
|
26
|
+
const credential = value.credential;
|
|
27
|
+
const result = {
|
|
28
|
+
version: 1,
|
|
29
|
+
credential: {
|
|
30
|
+
accessKey: requiredString(credential.accessKey),
|
|
31
|
+
secretKey: requiredString(credential.secretKey),
|
|
32
|
+
expiresAt: requiredDate(credential.expiresAt),
|
|
33
|
+
scope: requiredScope(credential.scope)
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
if (value.account !== undefined) {
|
|
37
|
+
if (!isRecord(value.account))
|
|
38
|
+
throw new Error("invalid account");
|
|
39
|
+
result.account = {
|
|
40
|
+
id: requiredString(value.account.id),
|
|
41
|
+
name: requiredString(value.account.name)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (value.organization !== undefined) {
|
|
45
|
+
if (!isRecord(value.organization))
|
|
46
|
+
throw new Error("invalid organization");
|
|
47
|
+
result.organization = organization(value.organization.id, value.organization.name, value.organization.bindExpiresAt);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function migrateVersionTwo(value) {
|
|
52
|
+
const result = {
|
|
53
|
+
version: 1,
|
|
54
|
+
credential: {
|
|
55
|
+
accessKey: requiredString(value.ak),
|
|
56
|
+
secretKey: requiredString(value.sk),
|
|
57
|
+
expiresAt: requiredDate(value.expires_at),
|
|
58
|
+
scope: normalizeScope(value.scope)
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const account = completeLegacyPair(value.user);
|
|
62
|
+
if (account)
|
|
63
|
+
result.account = account;
|
|
64
|
+
if (isRecord(value.current_org)) {
|
|
65
|
+
const id = legacyString(value.current_org.id);
|
|
66
|
+
const name = optionalString(value.current_org.name);
|
|
67
|
+
if (id && name) {
|
|
68
|
+
result.organization = organization(id, name, value.current_org.bind_expires_at);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function organization(id, name, expiresAt) {
|
|
74
|
+
const result = {
|
|
75
|
+
id: requiredString(id),
|
|
76
|
+
name: requiredString(name)
|
|
77
|
+
};
|
|
78
|
+
if (expiresAt !== undefined)
|
|
79
|
+
result.bindExpiresAt = requiredDate(expiresAt);
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function completeLegacyPair(value) {
|
|
83
|
+
if (!isRecord(value))
|
|
84
|
+
return undefined;
|
|
85
|
+
const id = legacyString(value.id);
|
|
86
|
+
const name = optionalString(value.name);
|
|
87
|
+
return id && name ? { id, name } : undefined;
|
|
88
|
+
}
|
|
89
|
+
function requiredScope(value) {
|
|
90
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
91
|
+
throw new Error("invalid scope");
|
|
92
|
+
}
|
|
93
|
+
return normalizeScope(value);
|
|
94
|
+
}
|
|
95
|
+
export function normalizeScope(value) {
|
|
96
|
+
if (value === undefined || value === null)
|
|
97
|
+
return [];
|
|
98
|
+
const entries = typeof value === "string" ? value.split(",") : value;
|
|
99
|
+
if (!Array.isArray(entries) || entries.some((item) => typeof item !== "string")) {
|
|
100
|
+
throw new Error("invalid scope");
|
|
101
|
+
}
|
|
102
|
+
return entries.map((item) => item.trim()).filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
function requiredDate(value) {
|
|
105
|
+
const text = requiredString(value);
|
|
106
|
+
if (Number.isNaN(Date.parse(text)))
|
|
107
|
+
throw new Error("invalid date");
|
|
108
|
+
return text;
|
|
109
|
+
}
|
|
110
|
+
function requiredString(value) {
|
|
111
|
+
const text = optionalString(value);
|
|
112
|
+
if (!text)
|
|
113
|
+
throw new Error("missing string");
|
|
114
|
+
return text;
|
|
115
|
+
}
|
|
116
|
+
function optionalString(value) {
|
|
117
|
+
if (typeof value !== "string")
|
|
118
|
+
return undefined;
|
|
119
|
+
const text = value.trim();
|
|
120
|
+
return text || undefined;
|
|
121
|
+
}
|
|
122
|
+
function legacyString(value) {
|
|
123
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
124
|
+
return String(value);
|
|
125
|
+
return optionalString(value);
|
|
126
|
+
}
|
|
127
|
+
function isRecord(value) {
|
|
128
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export class DeviceAuthorizationExpiredError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("授权码已过期,请重新执行 gd-cli auth login。");
|
|
4
|
+
this.name = "DeviceAuthorizationExpiredError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function createAuthUseCases(dependencies) {
|
|
8
|
+
return {
|
|
9
|
+
async status(input) {
|
|
10
|
+
const state = await dependencies.store.read();
|
|
11
|
+
if (!state)
|
|
12
|
+
return { logged_in: false, next_steps: ["gd-cli auth login"] };
|
|
13
|
+
const valid = Date.parse(state.credential.expiresAt) > input.now.getTime();
|
|
14
|
+
return {
|
|
15
|
+
logged_in: true,
|
|
16
|
+
account: {
|
|
17
|
+
user_id: state.account?.id ?? null,
|
|
18
|
+
name: state.account?.name ?? null
|
|
19
|
+
},
|
|
20
|
+
organization: state.organization
|
|
21
|
+
? { id: state.organization.id, name: state.organization.name }
|
|
22
|
+
: null,
|
|
23
|
+
credential: {
|
|
24
|
+
expires_at: state.credential.expiresAt,
|
|
25
|
+
valid,
|
|
26
|
+
scope: [...state.credential.scope]
|
|
27
|
+
},
|
|
28
|
+
next_steps: !valid
|
|
29
|
+
? ["gd-cli auth logout", "gd-cli auth login"]
|
|
30
|
+
: state.organization
|
|
31
|
+
? []
|
|
32
|
+
: ["gd-cli org list", "gd-cli org switch"]
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
async logout() {
|
|
36
|
+
await dependencies.store.remove();
|
|
37
|
+
},
|
|
38
|
+
async login(input) {
|
|
39
|
+
input.signal.throwIfAborted();
|
|
40
|
+
const existing = await dependencies.store.read();
|
|
41
|
+
if (existing && Date.parse(existing.credential.expiresAt) > dependencies.now().getTime()) {
|
|
42
|
+
return {
|
|
43
|
+
status: "already_logged_in",
|
|
44
|
+
organizationSelected: existing.organization !== undefined
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const authorization = await dependencies.sso.startDeviceAuthorization(input.signal);
|
|
48
|
+
const deadline = dependencies.now().getTime() + authorization.expiresInSeconds * 1000;
|
|
49
|
+
const authorizationUrl = new URL("/cli/device-authorization", dependencies.authPageOrigin);
|
|
50
|
+
authorizationUrl.searchParams.set("code", authorization.authorizeCode);
|
|
51
|
+
await input.interaction.showAuthorization(authorizationUrl);
|
|
52
|
+
if (!input.noBrowser) {
|
|
53
|
+
try {
|
|
54
|
+
await input.interaction.openBrowser(authorizationUrl);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
input.interaction.warn("无法自动打开浏览器,请手动访问授权链接。");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let interval = authorization.intervalSeconds * 1000;
|
|
61
|
+
while (true) {
|
|
62
|
+
if (dependencies.now().getTime() + interval > deadline) {
|
|
63
|
+
throw new DeviceAuthorizationExpiredError();
|
|
64
|
+
}
|
|
65
|
+
await dependencies.sleep(interval, input.signal);
|
|
66
|
+
input.signal.throwIfAborted();
|
|
67
|
+
const token = await dependencies.sso.pollDeviceToken(authorization.deviceCode, input.signal);
|
|
68
|
+
if (token.status === "pending")
|
|
69
|
+
continue;
|
|
70
|
+
if (token.status === "slow_down") {
|
|
71
|
+
interval += 5_000;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const state = {
|
|
75
|
+
version: 1,
|
|
76
|
+
credential: token.credential,
|
|
77
|
+
...(token.account ? { account: token.account } : {})
|
|
78
|
+
};
|
|
79
|
+
await dependencies.store.write(state);
|
|
80
|
+
if (!token.organizationId) {
|
|
81
|
+
input.interaction.warn("登录成功,请执行 gd-cli org switch 选择组织。");
|
|
82
|
+
return { status: "logged_in", organizationSelected: false };
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const organization = await dependencies.organizationBinder.bindAuthorizedOrganization({
|
|
86
|
+
credential: token.credential,
|
|
87
|
+
organizationId: token.organizationId,
|
|
88
|
+
signal: input.signal
|
|
89
|
+
});
|
|
90
|
+
if (!organization) {
|
|
91
|
+
input.interaction.warn("登录成功,但未能选择组织;请执行 gd-cli org switch。");
|
|
92
|
+
return { status: "logged_in", organizationSelected: false };
|
|
93
|
+
}
|
|
94
|
+
await dependencies.store.write({ ...state, organization });
|
|
95
|
+
return { status: "logged_in", organizationSelected: true };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
if (input.signal.aborted)
|
|
99
|
+
throw input.signal.reason;
|
|
100
|
+
input.interaction.warn("登录成功,但组织绑定失败;请执行 gd-cli org switch。");
|
|
101
|
+
return { status: "logged_in", organizationSelected: false };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|