@dayofweek/dcli 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -55
- package/dist/auth/login.d.ts +18 -0
- package/dist/auth/login.js +47 -0
- package/dist/auth/loopback.d.ts +6 -0
- package/dist/auth/loopback.js +59 -0
- package/dist/auth/pkce.d.ts +14 -0
- package/dist/auth/pkce.js +87 -0
- package/dist/bin/dcli.js +450 -57
- package/dist/client.d.ts +162 -2
- package/dist/client.js +214 -44
- package/dist/config.d.ts +13 -1
- package/dist/config.js +25 -7
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.js +113 -0
- package/dist/skills.d.ts +16 -0
- package/dist/skills.js +77 -0
- package/dist/uri.d.ts +9 -0
- package/dist/uri.js +72 -0
- package/package.json +9 -2
package/dist/client.d.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare class ApiError extends Error {
|
|
6
6
|
status: number;
|
|
7
|
-
|
|
7
|
+
code?: string | undefined;
|
|
8
|
+
requestId?: string | undefined;
|
|
9
|
+
constructor(status: number, message: string, code?: string | undefined, requestId?: string | undefined);
|
|
8
10
|
}
|
|
9
11
|
export declare class DayOfWeekClient {
|
|
10
12
|
private baseUrl;
|
|
@@ -13,6 +15,7 @@ export declare class DayOfWeekClient {
|
|
|
13
15
|
checkAuth(): Promise<{
|
|
14
16
|
status: string;
|
|
15
17
|
authenticated: boolean;
|
|
18
|
+
isAdmin?: boolean;
|
|
16
19
|
}>;
|
|
17
20
|
listDevices(): Promise<Array<{
|
|
18
21
|
_id: string;
|
|
@@ -27,6 +30,75 @@ export declare class DayOfWeekClient {
|
|
|
27
30
|
revokeDevice(deviceId: string): Promise<{
|
|
28
31
|
success: boolean;
|
|
29
32
|
}>;
|
|
33
|
+
revokeCurrentDevice(): Promise<{
|
|
34
|
+
revoked: true;
|
|
35
|
+
}>;
|
|
36
|
+
brainBootstrap(): Promise<{
|
|
37
|
+
authenticated: true;
|
|
38
|
+
scopes: string[];
|
|
39
|
+
entitled: boolean;
|
|
40
|
+
areas: BrainArea[];
|
|
41
|
+
defaultAreaId?: string;
|
|
42
|
+
}>;
|
|
43
|
+
listBrainAreas(): Promise<BrainArea[]>;
|
|
44
|
+
companyBrainStatus(): Promise<{
|
|
45
|
+
eligible: boolean;
|
|
46
|
+
canEnsure: boolean;
|
|
47
|
+
entityName?: string;
|
|
48
|
+
existingArea: BrainArea | null;
|
|
49
|
+
}>;
|
|
50
|
+
ensureCompanyBrain(): Promise<BrainArea>;
|
|
51
|
+
resolveBrain(uri: string): Promise<BrainResolvedResource>;
|
|
52
|
+
getBrainNote(id: string): Promise<BrainNote>;
|
|
53
|
+
getBrainSource(id: string): Promise<BrainSource>;
|
|
54
|
+
uploadBrainSource(input: {
|
|
55
|
+
areaId: string;
|
|
56
|
+
path: string;
|
|
57
|
+
mimeType: string;
|
|
58
|
+
isMeeting: boolean;
|
|
59
|
+
consentAcknowledged: boolean;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
sourceId: string;
|
|
62
|
+
uri: string;
|
|
63
|
+
httpsUrl: string;
|
|
64
|
+
filename: string;
|
|
65
|
+
byteSize: number;
|
|
66
|
+
sha256: string;
|
|
67
|
+
scanState: "pending" | "clean";
|
|
68
|
+
processingState: string;
|
|
69
|
+
}>;
|
|
70
|
+
downloadBrainSource(input: {
|
|
71
|
+
sourceId: string;
|
|
72
|
+
outputPath: string;
|
|
73
|
+
expectedSha256?: string;
|
|
74
|
+
overwrite?: boolean;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
outputPath: string;
|
|
77
|
+
byteSize: number;
|
|
78
|
+
sha256: string;
|
|
79
|
+
}>;
|
|
80
|
+
searchBrain(query: string, options?: {
|
|
81
|
+
areaId?: string;
|
|
82
|
+
limit?: number;
|
|
83
|
+
}): Promise<unknown>;
|
|
84
|
+
shareBrainNote(input: {
|
|
85
|
+
areaId: string;
|
|
86
|
+
title: string;
|
|
87
|
+
markdown: string;
|
|
88
|
+
sourceName?: string;
|
|
89
|
+
intent: "interactive" | "autonomous";
|
|
90
|
+
}): Promise<BrainNote>;
|
|
91
|
+
updateBrainNote(id: string, input: {
|
|
92
|
+
title?: string;
|
|
93
|
+
markdown: string;
|
|
94
|
+
expectedVersion: number;
|
|
95
|
+
}): Promise<BrainNote>;
|
|
96
|
+
archiveBrainNote(id: string, expectedVersion: number): Promise<BrainNote>;
|
|
97
|
+
restoreBrainNote(id: string, expectedVersion: number): Promise<BrainNote>;
|
|
98
|
+
listBrainAudit(areaId: string, options?: {
|
|
99
|
+
cursor?: string;
|
|
100
|
+
limit?: number;
|
|
101
|
+
}): Promise<unknown>;
|
|
30
102
|
listEntities(opts?: {
|
|
31
103
|
type?: string;
|
|
32
104
|
parent?: string;
|
|
@@ -57,20 +129,108 @@ export declare class DayOfWeekClient {
|
|
|
57
129
|
getProposal(proposalId: string): Promise<any>;
|
|
58
130
|
submitProposal(proposal: Record<string, unknown>): Promise<any>;
|
|
59
131
|
submitBatch(batch: {
|
|
132
|
+
org?: string;
|
|
60
133
|
batchLabel: string;
|
|
61
134
|
sourceAgent?: string;
|
|
62
135
|
proposals: any[];
|
|
63
136
|
}): Promise<any>;
|
|
64
|
-
|
|
137
|
+
adminListEntities(opts?: {
|
|
138
|
+
org?: string;
|
|
139
|
+
type?: string;
|
|
140
|
+
missingLocation?: boolean;
|
|
141
|
+
search?: string;
|
|
142
|
+
limit?: number;
|
|
143
|
+
}): Promise<{
|
|
144
|
+
results: any[];
|
|
145
|
+
truncated: boolean;
|
|
146
|
+
total: number;
|
|
147
|
+
}>;
|
|
148
|
+
adminListProposals(opts?: {
|
|
149
|
+
status?: string;
|
|
150
|
+
sourceAgent?: string;
|
|
151
|
+
limit?: number;
|
|
152
|
+
}): Promise<{
|
|
153
|
+
results: any[];
|
|
154
|
+
truncated: boolean;
|
|
155
|
+
total: number;
|
|
156
|
+
}>;
|
|
157
|
+
getSkillBundle(name?: string): Promise<{
|
|
65
158
|
name: string;
|
|
66
159
|
version: string;
|
|
160
|
+
hash?: string;
|
|
67
161
|
files: Array<{
|
|
68
162
|
path: string;
|
|
69
163
|
content: string;
|
|
164
|
+
sha256?: string;
|
|
70
165
|
}>;
|
|
71
166
|
}>;
|
|
167
|
+
listSkillBundles(): Promise<Array<{
|
|
168
|
+
name: string;
|
|
169
|
+
version: string;
|
|
170
|
+
hash: string;
|
|
171
|
+
}>>;
|
|
72
172
|
getSchema(): Promise<any>;
|
|
73
173
|
private get;
|
|
74
174
|
private post;
|
|
175
|
+
private patch;
|
|
75
176
|
private delete;
|
|
177
|
+
private request;
|
|
76
178
|
}
|
|
179
|
+
export type BrainArea = {
|
|
180
|
+
id: string;
|
|
181
|
+
name: string;
|
|
182
|
+
kind: "company" | "project";
|
|
183
|
+
role: "owner" | "editor" | "viewer";
|
|
184
|
+
canWrite: boolean;
|
|
185
|
+
canManage: boolean;
|
|
186
|
+
uri: string;
|
|
187
|
+
httpsUrl: string;
|
|
188
|
+
updatedAt: number;
|
|
189
|
+
};
|
|
190
|
+
export type BrainNote = {
|
|
191
|
+
id: string;
|
|
192
|
+
areaId: string;
|
|
193
|
+
areaName: string;
|
|
194
|
+
title: string;
|
|
195
|
+
markdown: string;
|
|
196
|
+
status: string;
|
|
197
|
+
version: number;
|
|
198
|
+
hash?: string;
|
|
199
|
+
sourceId?: string;
|
|
200
|
+
sourceUri?: string;
|
|
201
|
+
uri: string;
|
|
202
|
+
httpsUrl: string;
|
|
203
|
+
createdAt: number;
|
|
204
|
+
updatedAt: number;
|
|
205
|
+
};
|
|
206
|
+
export type BrainSource = {
|
|
207
|
+
id: string;
|
|
208
|
+
areaId: string;
|
|
209
|
+
areaName: string;
|
|
210
|
+
kind: "text" | "audio" | "file";
|
|
211
|
+
filename?: string;
|
|
212
|
+
mimeType?: string;
|
|
213
|
+
byteSize?: number;
|
|
214
|
+
sha256?: string;
|
|
215
|
+
scanState?: "pending" | "clean" | "quarantined" | "failed";
|
|
216
|
+
processingState: string;
|
|
217
|
+
rawText?: string;
|
|
218
|
+
extractedText?: string;
|
|
219
|
+
transcript?: string;
|
|
220
|
+
isMeeting: boolean;
|
|
221
|
+
derivedNoteUri?: string;
|
|
222
|
+
canDownload: boolean;
|
|
223
|
+
uri: string;
|
|
224
|
+
httpsUrl: string;
|
|
225
|
+
uploadedAt: number;
|
|
226
|
+
};
|
|
227
|
+
export type BrainResolvedResource = {
|
|
228
|
+
resourceType: "area";
|
|
229
|
+
area: BrainArea;
|
|
230
|
+
} | {
|
|
231
|
+
resourceType: "note";
|
|
232
|
+
note: BrainNote;
|
|
233
|
+
} | {
|
|
234
|
+
resourceType: "source";
|
|
235
|
+
source: BrainSource;
|
|
236
|
+
};
|
package/dist/client.js
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
* HTTP client for the Day of Week platform REST API.
|
|
3
3
|
* All calls go through the proxy at field.dayofweek.com/app/api/dcli.
|
|
4
4
|
*/
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { createReadStream, createWriteStream, existsSync, fsyncSync, linkSync, openSync, closeSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
|
+
import { Readable, Transform } from "node:stream";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
5
10
|
const DEFAULT_BASE_URL = "https://field.dayofweek.com/app/api/dcli";
|
|
6
11
|
export class ApiError extends Error {
|
|
7
12
|
status;
|
|
8
|
-
|
|
13
|
+
code;
|
|
14
|
+
requestId;
|
|
15
|
+
constructor(status, message, code, requestId) {
|
|
9
16
|
super(message);
|
|
10
17
|
this.status = status;
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.requestId = requestId;
|
|
11
20
|
this.name = "ApiError";
|
|
12
21
|
}
|
|
13
22
|
}
|
|
@@ -31,6 +40,145 @@ export class DayOfWeekClient {
|
|
|
31
40
|
async revokeDevice(deviceId) {
|
|
32
41
|
return this.delete(`/auth/devices/${deviceId}`);
|
|
33
42
|
}
|
|
43
|
+
async revokeCurrentDevice() {
|
|
44
|
+
return this.delete("/auth/current-device");
|
|
45
|
+
}
|
|
46
|
+
// ── Shared brain ─────────────────────────────────────────────────────────
|
|
47
|
+
async brainBootstrap() {
|
|
48
|
+
return this.get("/brain/bootstrap");
|
|
49
|
+
}
|
|
50
|
+
async listBrainAreas() {
|
|
51
|
+
return this.get("/brain/areas");
|
|
52
|
+
}
|
|
53
|
+
async companyBrainStatus() {
|
|
54
|
+
return this.get("/brain/company-area");
|
|
55
|
+
}
|
|
56
|
+
async ensureCompanyBrain() {
|
|
57
|
+
return this.post("/brain/company-area", {});
|
|
58
|
+
}
|
|
59
|
+
async resolveBrain(uri) {
|
|
60
|
+
return this.get(`/brain/resolve?uri=${encodeURIComponent(uri)}`);
|
|
61
|
+
}
|
|
62
|
+
async getBrainNote(id) {
|
|
63
|
+
return this.get(`/brain/notes/${encodeURIComponent(id)}`);
|
|
64
|
+
}
|
|
65
|
+
async getBrainSource(id) {
|
|
66
|
+
return this.get(`/brain/sources/${encodeURIComponent(id)}`);
|
|
67
|
+
}
|
|
68
|
+
async uploadBrainSource(input) {
|
|
69
|
+
const info = statSync(input.path);
|
|
70
|
+
if (!info.isFile())
|
|
71
|
+
throw new Error("Upload path is not a regular file");
|
|
72
|
+
const sha256 = await hashFile(input.path);
|
|
73
|
+
const idempotencyKey = `up_${createHash("sha256")
|
|
74
|
+
.update(`${input.areaId}:${sha256}:${input.isMeeting}`)
|
|
75
|
+
.digest("base64url")}`;
|
|
76
|
+
const session = await this.post("/brain/source-uploads", {
|
|
77
|
+
areaId: input.areaId,
|
|
78
|
+
filename: basename(input.path),
|
|
79
|
+
mimeType: input.mimeType,
|
|
80
|
+
byteSize: info.size,
|
|
81
|
+
sha256,
|
|
82
|
+
isMeeting: input.isMeeting,
|
|
83
|
+
consentAcknowledged: input.consentAcknowledged,
|
|
84
|
+
idempotencyKey,
|
|
85
|
+
});
|
|
86
|
+
const upload = await fetch(session.uploadUrl, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": input.mimeType },
|
|
89
|
+
body: createReadStream(input.path),
|
|
90
|
+
duplex: "half",
|
|
91
|
+
});
|
|
92
|
+
if (!upload.ok)
|
|
93
|
+
throw new ApiError(upload.status, "Source byte upload failed", "network_error");
|
|
94
|
+
const stored = await upload.json();
|
|
95
|
+
if (!stored.storageId)
|
|
96
|
+
throw new Error("Upload did not return a storage ID");
|
|
97
|
+
const completed = await this.post(`/brain/source-uploads/${encodeURIComponent(session.uploadId)}/complete`, {
|
|
98
|
+
storageId: stored.storageId,
|
|
99
|
+
});
|
|
100
|
+
if (completed.sha256 !== sha256)
|
|
101
|
+
throw new Error("Server checksum does not match the uploaded file");
|
|
102
|
+
return completed;
|
|
103
|
+
}
|
|
104
|
+
async downloadBrainSource(input) {
|
|
105
|
+
if (!input.overwrite && existsSync(input.outputPath))
|
|
106
|
+
throw new Error("Output already exists; use --overwrite");
|
|
107
|
+
const response = await fetch(`${this.baseUrl}/brain/sources/${encodeURIComponent(input.sourceId)}/download`, {
|
|
108
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
109
|
+
});
|
|
110
|
+
if (!response.ok)
|
|
111
|
+
throw await apiErrorFromResponse(response);
|
|
112
|
+
if (!response.body)
|
|
113
|
+
throw new Error("Download response had no body");
|
|
114
|
+
const expected = response.headers.get("x-dayofweek-sha256") ?? input.expectedSha256;
|
|
115
|
+
if (!expected || !/^[a-f0-9]{64}$/.test(expected))
|
|
116
|
+
throw new Error("Download response omitted its checksum");
|
|
117
|
+
const tempPath = join(dirname(input.outputPath), `.${basename(input.outputPath)}.dcli-${randomUUID()}.tmp`);
|
|
118
|
+
const hash = createHash("sha256");
|
|
119
|
+
let byteSize = 0;
|
|
120
|
+
const hasher = new Transform({
|
|
121
|
+
transform(chunk, _encoding, callback) {
|
|
122
|
+
hash.update(chunk);
|
|
123
|
+
byteSize += chunk.length;
|
|
124
|
+
callback(null, chunk);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
await pipeline(Readable.fromWeb(response.body), hasher, createWriteStream(tempPath, { flags: "wx", mode: 0o600 }));
|
|
129
|
+
const descriptor = openSync(tempPath, "r");
|
|
130
|
+
try {
|
|
131
|
+
fsyncSync(descriptor);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
closeSync(descriptor);
|
|
135
|
+
}
|
|
136
|
+
const actual = hash.digest("hex");
|
|
137
|
+
if (actual !== expected || (input.expectedSha256 && actual !== input.expectedSha256)) {
|
|
138
|
+
throw new Error("Downloaded source checksum mismatch");
|
|
139
|
+
}
|
|
140
|
+
if (input.overwrite) {
|
|
141
|
+
renameSync(tempPath, input.outputPath);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
linkSync(tempPath, input.outputPath);
|
|
145
|
+
unlinkSync(tempPath);
|
|
146
|
+
}
|
|
147
|
+
return { outputPath: input.outputPath, byteSize, sha256: actual };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (existsSync(tempPath))
|
|
151
|
+
unlinkSync(tempPath);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async searchBrain(query, options) {
|
|
156
|
+
return this.post("/brain/search", {
|
|
157
|
+
query,
|
|
158
|
+
areaId: options?.areaId,
|
|
159
|
+
limit: options?.limit,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async shareBrainNote(input) {
|
|
163
|
+
return this.post("/brain/notes", input);
|
|
164
|
+
}
|
|
165
|
+
async updateBrainNote(id, input) {
|
|
166
|
+
return this.patch(`/brain/notes/${encodeURIComponent(id)}`, input);
|
|
167
|
+
}
|
|
168
|
+
async archiveBrainNote(id, expectedVersion) {
|
|
169
|
+
return this.delete(`/brain/notes/${encodeURIComponent(id)}?expectedVersion=${expectedVersion}`);
|
|
170
|
+
}
|
|
171
|
+
async restoreBrainNote(id, expectedVersion) {
|
|
172
|
+
return this.post(`/brain/notes/${encodeURIComponent(id)}/restore`, { expectedVersion });
|
|
173
|
+
}
|
|
174
|
+
async listBrainAudit(areaId, options) {
|
|
175
|
+
const query = new URLSearchParams({ areaId });
|
|
176
|
+
if (options?.cursor)
|
|
177
|
+
query.set("cursor", options.cursor);
|
|
178
|
+
if (options?.limit)
|
|
179
|
+
query.set("limit", String(options.limit));
|
|
180
|
+
return this.get(`/brain/audit?${query}`);
|
|
181
|
+
}
|
|
34
182
|
// ── Read ──────────────────────────────────────────────────────────────────
|
|
35
183
|
async listEntities(opts) {
|
|
36
184
|
const params = new URLSearchParams();
|
|
@@ -103,9 +251,40 @@ export class DayOfWeekClient {
|
|
|
103
251
|
async submitBatch(batch) {
|
|
104
252
|
return this.post("/proposals/batch", batch);
|
|
105
253
|
}
|
|
254
|
+
// ── Admin (cross-org) ─────────────────────────────────────────────────────
|
|
255
|
+
// These endpoints reject non-admin tokens with 401 "Admin access required".
|
|
256
|
+
async adminListEntities(opts) {
|
|
257
|
+
const params = new URLSearchParams();
|
|
258
|
+
if (opts?.org)
|
|
259
|
+
params.set("org", opts.org);
|
|
260
|
+
if (opts?.type)
|
|
261
|
+
params.set("type", opts.type);
|
|
262
|
+
if (opts?.missingLocation)
|
|
263
|
+
params.set("missing-location", "1");
|
|
264
|
+
if (opts?.search)
|
|
265
|
+
params.set("search", opts.search);
|
|
266
|
+
if (opts?.limit)
|
|
267
|
+
params.set("limit", String(opts.limit));
|
|
268
|
+
const qs = params.toString();
|
|
269
|
+
return this.get(`/admin/entities${qs ? `?${qs}` : ""}`);
|
|
270
|
+
}
|
|
271
|
+
async adminListProposals(opts) {
|
|
272
|
+
const params = new URLSearchParams();
|
|
273
|
+
if (opts?.status)
|
|
274
|
+
params.set("status", opts.status);
|
|
275
|
+
if (opts?.sourceAgent)
|
|
276
|
+
params.set("source-agent", opts.sourceAgent);
|
|
277
|
+
if (opts?.limit)
|
|
278
|
+
params.set("limit", String(opts.limit));
|
|
279
|
+
const qs = params.toString();
|
|
280
|
+
return this.get(`/admin/proposals${qs ? `?${qs}` : ""}`);
|
|
281
|
+
}
|
|
106
282
|
// ── Skill ─────────────────────────────────────────────────────────────────
|
|
107
|
-
async getSkillBundle() {
|
|
108
|
-
return this.get(
|
|
283
|
+
async getSkillBundle(name) {
|
|
284
|
+
return this.get(`/skill${name ? `?name=${encodeURIComponent(name)}` : ""}`);
|
|
285
|
+
}
|
|
286
|
+
async listSkillBundles() {
|
|
287
|
+
return this.get("/skill?list=1");
|
|
109
288
|
}
|
|
110
289
|
// ── Schema ────────────────────────────────────────────────────────────────
|
|
111
290
|
async getSchema() {
|
|
@@ -113,55 +292,46 @@ export class DayOfWeekClient {
|
|
|
113
292
|
}
|
|
114
293
|
// ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
115
294
|
async get(path) {
|
|
116
|
-
|
|
117
|
-
headers: { Authorization: `Bearer ${this.token}` },
|
|
118
|
-
});
|
|
119
|
-
if (!res.ok) {
|
|
120
|
-
const body = await res.text();
|
|
121
|
-
try {
|
|
122
|
-
const json = JSON.parse(body);
|
|
123
|
-
throw new ApiError(res.status, json.error ?? body);
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
if (e instanceof ApiError)
|
|
127
|
-
throw e;
|
|
128
|
-
throw new ApiError(res.status, body);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return res.json();
|
|
295
|
+
return this.request(path, { method: "GET" });
|
|
132
296
|
}
|
|
133
297
|
async post(path, body) {
|
|
298
|
+
return this.request(path, { method: "POST", body: JSON.stringify(body) });
|
|
299
|
+
}
|
|
300
|
+
async patch(path, body) {
|
|
301
|
+
return this.request(path, { method: "PATCH", body: JSON.stringify(body) });
|
|
302
|
+
}
|
|
303
|
+
async delete(path) {
|
|
304
|
+
return this.request(path, { method: "DELETE" });
|
|
305
|
+
}
|
|
306
|
+
async request(path, init) {
|
|
134
307
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
135
|
-
|
|
308
|
+
...init,
|
|
136
309
|
headers: {
|
|
137
310
|
Authorization: `Bearer ${this.token}`,
|
|
138
|
-
"Content-Type": "application/json",
|
|
311
|
+
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
312
|
+
...init.headers,
|
|
139
313
|
},
|
|
140
|
-
body: JSON.stringify(body),
|
|
141
314
|
});
|
|
142
|
-
if (!res.ok)
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const json = JSON.parse(text);
|
|
146
|
-
throw new ApiError(res.status, json.error ?? text);
|
|
147
|
-
}
|
|
148
|
-
catch (e) {
|
|
149
|
-
if (e instanceof ApiError)
|
|
150
|
-
throw e;
|
|
151
|
-
throw new ApiError(res.status, text);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
315
|
+
if (!res.ok)
|
|
316
|
+
throw await apiErrorFromResponse(res);
|
|
154
317
|
return res.json();
|
|
155
318
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
319
|
+
}
|
|
320
|
+
async function hashFile(path) {
|
|
321
|
+
const hash = createHash("sha256");
|
|
322
|
+
for await (const chunk of createReadStream(path))
|
|
323
|
+
hash.update(chunk);
|
|
324
|
+
return hash.digest("hex");
|
|
325
|
+
}
|
|
326
|
+
async function apiErrorFromResponse(response) {
|
|
327
|
+
const body = await response.text();
|
|
328
|
+
try {
|
|
329
|
+
const json = JSON.parse(body);
|
|
330
|
+
if (typeof json.error === "string")
|
|
331
|
+
return new ApiError(response.status, json.error);
|
|
332
|
+
return new ApiError(response.status, json.error?.message ?? `Request failed (${response.status})`, json.error?.code, json.error?.requestId);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return new ApiError(response.status, `Request failed (${response.status})`);
|
|
166
336
|
}
|
|
167
337
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
import { type CredentialStore } from "./credentials.js";
|
|
1
2
|
export interface DcliConfig {
|
|
2
3
|
authToken?: string;
|
|
3
4
|
apiUrl?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Cached admin status from the last `dcli auth status` call. Used to
|
|
7
|
+
* decide locally whether to expose admin-only subcommands in `--help`.
|
|
8
|
+
* Refreshed automatically when `dcli auth status` or `dcli auth login`
|
|
9
|
+
* runs; admin commands are hidden when this is unset or false.
|
|
10
|
+
*/
|
|
11
|
+
isAdmin?: boolean;
|
|
12
|
+
/** Unix-ms timestamp of the last isAdmin refresh. */
|
|
13
|
+
roleCachedAt?: number;
|
|
4
14
|
}
|
|
5
15
|
export declare function loadConfig(): DcliConfig;
|
|
6
16
|
export declare function saveConfig(updates: Partial<DcliConfig>): void;
|
|
7
|
-
export declare function getToken(): string;
|
|
17
|
+
export declare function getToken(store?: CredentialStore): string;
|
|
18
|
+
export declare function saveCredential(secret: string, store?: CredentialStore): void;
|
|
19
|
+
export declare function deleteCredential(store?: CredentialStore): void;
|
|
8
20
|
export declare function getApiUrl(): string;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { defaultCredentialStore } from "./credentials.js";
|
|
4
5
|
const CONFIG_DIR = join(homedir(), ".config", "dayofweek");
|
|
5
6
|
const CONFIG_FILE = join(CONFIG_DIR, "dcli.json");
|
|
6
7
|
export function loadConfig() {
|
|
@@ -15,22 +16,39 @@ export function loadConfig() {
|
|
|
15
16
|
return {};
|
|
16
17
|
}
|
|
17
18
|
export function saveConfig(updates) {
|
|
18
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
19
20
|
const existing = loadConfig();
|
|
20
21
|
const merged = { ...existing, ...updates };
|
|
21
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
|
|
22
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
22
23
|
}
|
|
23
|
-
export function getToken() {
|
|
24
|
+
export function getToken(store = defaultCredentialStore()) {
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const storedToken = store.get();
|
|
24
27
|
const token = process.env.DCLI_AUTH_TOKEN ??
|
|
25
28
|
process.env.DCLI_TOKEN ??
|
|
26
|
-
|
|
29
|
+
storedToken ??
|
|
30
|
+
config.authToken;
|
|
31
|
+
// One-way migration of the old plaintext config. The legacy key is removed
|
|
32
|
+
// immediately after the protected store accepts it.
|
|
33
|
+
if (!storedToken && config.authToken && token === config.authToken) {
|
|
34
|
+
store.set(config.authToken);
|
|
35
|
+
saveConfig({ authToken: undefined });
|
|
36
|
+
}
|
|
27
37
|
if (!token) {
|
|
28
|
-
|
|
29
|
-
console.error("Set DCLI_AUTH_TOKEN or run: dcli auth login");
|
|
30
|
-
process.exit(1);
|
|
38
|
+
throw new Error("No credential found. Run: dcli auth login");
|
|
31
39
|
}
|
|
32
40
|
return token;
|
|
33
41
|
}
|
|
42
|
+
export function saveCredential(secret, store = defaultCredentialStore()) {
|
|
43
|
+
store.set(secret);
|
|
44
|
+
if (loadConfig().authToken)
|
|
45
|
+
saveConfig({ authToken: undefined });
|
|
46
|
+
}
|
|
47
|
+
export function deleteCredential(store = defaultCredentialStore()) {
|
|
48
|
+
store.delete();
|
|
49
|
+
if (loadConfig().authToken)
|
|
50
|
+
saveConfig({ authToken: undefined });
|
|
51
|
+
}
|
|
34
52
|
export function getApiUrl() {
|
|
35
53
|
return (process.env.DCLI_API_URL ??
|
|
36
54
|
loadConfig().apiUrl ??
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CredentialStore {
|
|
2
|
+
get(): string | undefined;
|
|
3
|
+
set(secret: string): void;
|
|
4
|
+
delete(): void;
|
|
5
|
+
}
|
|
6
|
+
export declare class ProtectedFileCredentialStore implements CredentialStore {
|
|
7
|
+
readonly path: string;
|
|
8
|
+
constructor(path?: string);
|
|
9
|
+
get(): string | undefined;
|
|
10
|
+
set(secret: string): void;
|
|
11
|
+
delete(): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function defaultCredentialStore(): CredentialStore;
|