@dayofweek/dcli 1.3.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 +391 -58
- package/dist/client.d.ts +141 -2
- package/dist/client.js +186 -44
- package/dist/config.d.ts +4 -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;
|
|
@@ -28,6 +30,75 @@ export declare class DayOfWeekClient {
|
|
|
28
30
|
revokeDevice(deviceId: string): Promise<{
|
|
29
31
|
success: boolean;
|
|
30
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>;
|
|
31
102
|
listEntities(opts?: {
|
|
32
103
|
type?: string;
|
|
33
104
|
parent?: string;
|
|
@@ -58,6 +129,7 @@ export declare class DayOfWeekClient {
|
|
|
58
129
|
getProposal(proposalId: string): Promise<any>;
|
|
59
130
|
submitProposal(proposal: Record<string, unknown>): Promise<any>;
|
|
60
131
|
submitBatch(batch: {
|
|
132
|
+
org?: string;
|
|
61
133
|
batchLabel: string;
|
|
62
134
|
sourceAgent?: string;
|
|
63
135
|
proposals: any[];
|
|
@@ -82,16 +154,83 @@ export declare class DayOfWeekClient {
|
|
|
82
154
|
truncated: boolean;
|
|
83
155
|
total: number;
|
|
84
156
|
}>;
|
|
85
|
-
getSkillBundle(): Promise<{
|
|
157
|
+
getSkillBundle(name?: string): Promise<{
|
|
86
158
|
name: string;
|
|
87
159
|
version: string;
|
|
160
|
+
hash?: string;
|
|
88
161
|
files: Array<{
|
|
89
162
|
path: string;
|
|
90
163
|
content: string;
|
|
164
|
+
sha256?: string;
|
|
91
165
|
}>;
|
|
92
166
|
}>;
|
|
167
|
+
listSkillBundles(): Promise<Array<{
|
|
168
|
+
name: string;
|
|
169
|
+
version: string;
|
|
170
|
+
hash: string;
|
|
171
|
+
}>>;
|
|
93
172
|
getSchema(): Promise<any>;
|
|
94
173
|
private get;
|
|
95
174
|
private post;
|
|
175
|
+
private patch;
|
|
96
176
|
private delete;
|
|
177
|
+
private request;
|
|
97
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();
|
|
@@ -132,8 +280,11 @@ export class DayOfWeekClient {
|
|
|
132
280
|
return this.get(`/admin/proposals${qs ? `?${qs}` : ""}`);
|
|
133
281
|
}
|
|
134
282
|
// ── Skill ─────────────────────────────────────────────────────────────────
|
|
135
|
-
async getSkillBundle() {
|
|
136
|
-
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");
|
|
137
288
|
}
|
|
138
289
|
// ── Schema ────────────────────────────────────────────────────────────────
|
|
139
290
|
async getSchema() {
|
|
@@ -141,55 +292,46 @@ export class DayOfWeekClient {
|
|
|
141
292
|
}
|
|
142
293
|
// ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
143
294
|
async get(path) {
|
|
144
|
-
|
|
145
|
-
headers: { Authorization: `Bearer ${this.token}` },
|
|
146
|
-
});
|
|
147
|
-
if (!res.ok) {
|
|
148
|
-
const body = await res.text();
|
|
149
|
-
try {
|
|
150
|
-
const json = JSON.parse(body);
|
|
151
|
-
throw new ApiError(res.status, json.error ?? body);
|
|
152
|
-
}
|
|
153
|
-
catch (e) {
|
|
154
|
-
if (e instanceof ApiError)
|
|
155
|
-
throw e;
|
|
156
|
-
throw new ApiError(res.status, body);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return res.json();
|
|
295
|
+
return this.request(path, { method: "GET" });
|
|
160
296
|
}
|
|
161
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) {
|
|
162
307
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
163
|
-
|
|
308
|
+
...init,
|
|
164
309
|
headers: {
|
|
165
310
|
Authorization: `Bearer ${this.token}`,
|
|
166
|
-
"Content-Type": "application/json",
|
|
311
|
+
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
312
|
+
...init.headers,
|
|
167
313
|
},
|
|
168
|
-
body: JSON.stringify(body),
|
|
169
314
|
});
|
|
170
|
-
if (!res.ok)
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
const json = JSON.parse(text);
|
|
174
|
-
throw new ApiError(res.status, json.error ?? text);
|
|
175
|
-
}
|
|
176
|
-
catch (e) {
|
|
177
|
-
if (e instanceof ApiError)
|
|
178
|
-
throw e;
|
|
179
|
-
throw new ApiError(res.status, text);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
315
|
+
if (!res.ok)
|
|
316
|
+
throw await apiErrorFromResponse(res);
|
|
182
317
|
return res.json();
|
|
183
318
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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})`);
|
|
194
336
|
}
|
|
195
337
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CredentialStore } from "./credentials.js";
|
|
1
2
|
export interface DcliConfig {
|
|
2
3
|
authToken?: string;
|
|
3
4
|
apiUrl?: string;
|
|
@@ -13,5 +14,7 @@ export interface DcliConfig {
|
|
|
13
14
|
}
|
|
14
15
|
export declare function loadConfig(): DcliConfig;
|
|
15
16
|
export declare function saveConfig(updates: Partial<DcliConfig>): void;
|
|
16
|
-
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;
|
|
17
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;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { homedir, platform, userInfo } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
const SERVICE = "com.dayofweek.dcli";
|
|
6
|
+
const ACCOUNT = "brain-device";
|
|
7
|
+
export class ProtectedFileCredentialStore {
|
|
8
|
+
path;
|
|
9
|
+
constructor(path = join(homedir(), ".config", "dayofweek", "credentials.json")) {
|
|
10
|
+
this.path = path;
|
|
11
|
+
}
|
|
12
|
+
get() {
|
|
13
|
+
if (!existsSync(this.path))
|
|
14
|
+
return undefined;
|
|
15
|
+
if (platform() !== "win32" && (statSync(this.path).mode & 0o077) !== 0) {
|
|
16
|
+
throw new Error(`Credential file permissions are unsafe: ${this.path}`);
|
|
17
|
+
}
|
|
18
|
+
const value = JSON.parse(readFileSync(this.path, "utf8"));
|
|
19
|
+
return typeof value.deviceSecret === "string" ? value.deviceSecret : undefined;
|
|
20
|
+
}
|
|
21
|
+
set(secret) {
|
|
22
|
+
if (!/^dsk_[A-Za-z0-9_-]{20,160}$/.test(secret))
|
|
23
|
+
throw new Error("Refusing to store an invalid credential");
|
|
24
|
+
const directory = dirname(this.path);
|
|
25
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
26
|
+
if (platform() !== "win32")
|
|
27
|
+
chmodSync(directory, 0o700);
|
|
28
|
+
writeFileSync(this.path, JSON.stringify({ deviceSecret: secret }), {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
mode: 0o600,
|
|
31
|
+
flag: "w",
|
|
32
|
+
});
|
|
33
|
+
if (platform() !== "win32") {
|
|
34
|
+
chmodSync(this.path, 0o600);
|
|
35
|
+
if ((statSync(this.path).mode & 0o077) !== 0)
|
|
36
|
+
throw new Error("Could not secure credential file");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const username = userInfo().username;
|
|
40
|
+
execFileSync("icacls", [this.path, "/inheritance:r", "/grant:r", `${username}:(R,W)`], {
|
|
41
|
+
stdio: "ignore",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
delete() {
|
|
46
|
+
if (existsSync(this.path))
|
|
47
|
+
unlinkSync(this.path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class MacKeychainCredentialStore {
|
|
51
|
+
get() {
|
|
52
|
+
try {
|
|
53
|
+
return execFileSync("security", ["find-generic-password", "-s", SERVICE, "-a", ACCOUNT, "-w"], {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
56
|
+
}).trim() || undefined;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
set(secret) {
|
|
63
|
+
execFileSync("security", ["add-generic-password", "-U", "-s", SERVICE, "-a", ACCOUNT, "-w", secret], { stdio: "ignore" });
|
|
64
|
+
}
|
|
65
|
+
delete() {
|
|
66
|
+
try {
|
|
67
|
+
execFileSync("security", ["delete-generic-password", "-s", SERVICE, "-a", ACCOUNT], {
|
|
68
|
+
stdio: "ignore",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Missing credential is already logged out.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
class LinuxSecretServiceCredentialStore {
|
|
77
|
+
get() {
|
|
78
|
+
try {
|
|
79
|
+
return execFileSync("secret-tool", ["lookup", "service", SERVICE, "account", ACCOUNT], {
|
|
80
|
+
encoding: "utf8",
|
|
81
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
82
|
+
}).trim() || undefined;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
set(secret) {
|
|
89
|
+
execFileSync("secret-tool", ["store", "--label", "Day of Week device", "service", SERVICE, "account", ACCOUNT], { input: secret, stdio: ["pipe", "ignore", "ignore"] });
|
|
90
|
+
}
|
|
91
|
+
delete() {
|
|
92
|
+
try {
|
|
93
|
+
execFileSync("secret-tool", ["clear", "service", SERVICE, "account", ACCOUNT], { stdio: "ignore" });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Missing credential is already logged out.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function defaultCredentialStore() {
|
|
101
|
+
if (platform() === "darwin")
|
|
102
|
+
return new MacKeychainCredentialStore();
|
|
103
|
+
if (platform() === "linux") {
|
|
104
|
+
try {
|
|
105
|
+
execFileSync("secret-tool", ["--version"], { stdio: "ignore" });
|
|
106
|
+
return new LinuxSecretServiceCredentialStore();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return new ProtectedFileCredentialStore();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return new ProtectedFileCredentialStore();
|
|
113
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SkillBundle = {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string;
|
|
4
|
+
hash?: string;
|
|
5
|
+
files: Array<{
|
|
6
|
+
path: string;
|
|
7
|
+
content: string;
|
|
8
|
+
sha256?: string;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
export declare function validateSkillBundle(bundle: SkillBundle): SkillBundle;
|
|
12
|
+
export declare function writeSkillBundle(input: SkillBundle, targetDir: string): {
|
|
13
|
+
written: number;
|
|
14
|
+
unchanged: number;
|
|
15
|
+
conflicts: string[];
|
|
16
|
+
};
|