@dayofweek/dcli 1.3.0 → 1.5.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/dist/client.d.ts CHANGED
@@ -2,9 +2,16 @@
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
+ /**
6
+ * Node's Buffer is a Uint8Array view over a possibly-larger, possibly-shared
7
+ * backing store, which is not assignable to BodyInit. Copy out the exact bytes.
8
+ */
9
+ export declare function toArrayBuffer(view: Uint8Array): ArrayBuffer;
5
10
  export declare class ApiError extends Error {
6
11
  status: number;
7
- constructor(status: number, message: string);
12
+ code?: string | undefined;
13
+ requestId?: string | undefined;
14
+ constructor(status: number, message: string, code?: string | undefined, requestId?: string | undefined);
8
15
  }
9
16
  export declare class DayOfWeekClient {
10
17
  private baseUrl;
@@ -28,6 +35,75 @@ export declare class DayOfWeekClient {
28
35
  revokeDevice(deviceId: string): Promise<{
29
36
  success: boolean;
30
37
  }>;
38
+ revokeCurrentDevice(): Promise<{
39
+ revoked: true;
40
+ }>;
41
+ brainBootstrap(): Promise<{
42
+ authenticated: true;
43
+ scopes: string[];
44
+ entitled: boolean;
45
+ areas: BrainArea[];
46
+ defaultAreaId?: string;
47
+ }>;
48
+ listBrainAreas(): Promise<BrainArea[]>;
49
+ companyBrainStatus(): Promise<{
50
+ eligible: boolean;
51
+ canEnsure: boolean;
52
+ entityName?: string;
53
+ existingArea: BrainArea | null;
54
+ }>;
55
+ ensureCompanyBrain(): Promise<BrainArea>;
56
+ resolveBrain(uri: string): Promise<BrainResolvedResource>;
57
+ getBrainNote(id: string): Promise<BrainNote>;
58
+ getBrainSource(id: string): Promise<BrainSource>;
59
+ uploadBrainSource(input: {
60
+ areaId: string;
61
+ path: string;
62
+ mimeType: string;
63
+ isMeeting: boolean;
64
+ consentAcknowledged: boolean;
65
+ }): Promise<{
66
+ sourceId: string;
67
+ uri: string;
68
+ httpsUrl: string;
69
+ filename: string;
70
+ byteSize: number;
71
+ sha256: string;
72
+ scanState: "pending" | "clean";
73
+ processingState: string;
74
+ }>;
75
+ downloadBrainSource(input: {
76
+ sourceId: string;
77
+ outputPath: string;
78
+ expectedSha256?: string;
79
+ overwrite?: boolean;
80
+ }): Promise<{
81
+ outputPath: string;
82
+ byteSize: number;
83
+ sha256: string;
84
+ }>;
85
+ searchBrain(query: string, options?: {
86
+ areaId?: string;
87
+ limit?: number;
88
+ }): Promise<unknown>;
89
+ shareBrainNote(input: {
90
+ areaId: string;
91
+ title: string;
92
+ markdown: string;
93
+ sourceName?: string;
94
+ intent: "interactive" | "autonomous";
95
+ }): Promise<BrainNote>;
96
+ updateBrainNote(id: string, input: {
97
+ title?: string;
98
+ markdown: string;
99
+ expectedVersion: number;
100
+ }): Promise<BrainNote>;
101
+ archiveBrainNote(id: string, expectedVersion: number): Promise<BrainNote>;
102
+ restoreBrainNote(id: string, expectedVersion: number): Promise<BrainNote>;
103
+ listBrainAudit(areaId: string, options?: {
104
+ cursor?: string;
105
+ limit?: number;
106
+ }): Promise<unknown>;
31
107
  listEntities(opts?: {
32
108
  type?: string;
33
109
  parent?: string;
@@ -58,6 +134,7 @@ export declare class DayOfWeekClient {
58
134
  getProposal(proposalId: string): Promise<any>;
59
135
  submitProposal(proposal: Record<string, unknown>): Promise<any>;
60
136
  submitBatch(batch: {
137
+ org?: string;
61
138
  batchLabel: string;
62
139
  sourceAgent?: string;
63
140
  proposals: any[];
@@ -82,16 +159,142 @@ export declare class DayOfWeekClient {
82
159
  truncated: boolean;
83
160
  total: number;
84
161
  }>;
85
- getSkillBundle(): Promise<{
162
+ /**
163
+ * List the knowledge documents attached to an entity. `full` returns each
164
+ * document's whole content instead of an excerpt, which is what you want when
165
+ * mirroring an entity's sources into an external knowledge base.
166
+ */
167
+ listKnowledge(opts: {
168
+ entity: string;
169
+ full?: boolean;
170
+ org?: string;
171
+ }): Promise<any[]>;
172
+ getKnowledge(documentId: string, org?: string): Promise<any>;
173
+ /** Semantic search across knowledge documents. */
174
+ searchKnowledge(opts: {
175
+ query: string;
176
+ entity?: string;
177
+ types?: string[];
178
+ limit?: number;
179
+ allOrgs?: boolean;
180
+ org?: string;
181
+ }): Promise<any>;
182
+ /**
183
+ * Add a markdown knowledge note.
184
+ *
185
+ * Without `direct` this submits a proposal for human review — the default,
186
+ * and the only option non-admin tokens have. With `direct: true` an admin
187
+ * token writes the document immediately, skipping review. Ask the operator
188
+ * before doing that; see references/admin.md in the served skill.
189
+ */
190
+ addKnowledge(input: {
191
+ entityId: string;
192
+ title: string;
193
+ content: string;
194
+ sourceType?: string;
195
+ sourceUrl?: string;
196
+ sourceDescription?: string;
197
+ confidence?: number;
198
+ sourceAgent?: string;
199
+ direct?: boolean;
200
+ org?: string;
201
+ }): Promise<any>;
202
+ /**
203
+ * Attach a binary source (PDF, DOCX, XLSX, …) to an entity. Admin only.
204
+ *
205
+ * Three hops: ask for an upload URL, send the bytes straight to Convex
206
+ * storage, then register the resulting storageId. The bytes never pass
207
+ * through function arguments, so file size isn't bounded by an arg limit.
208
+ */
209
+ attachKnowledgeFile(input: {
210
+ entityId: string;
211
+ /** Raw file bytes. Buffer callers: pass `toArrayBuffer(buf)` below. */
212
+ data: ArrayBuffer;
213
+ fileName: string;
214
+ mimeType: string;
215
+ sourceType?: string;
216
+ sourceUrl?: string;
217
+ sourceDescription?: string;
218
+ org?: string;
219
+ }): Promise<any>;
220
+ getSkillBundle(name?: string): Promise<{
86
221
  name: string;
87
222
  version: string;
223
+ hash?: string;
88
224
  files: Array<{
89
225
  path: string;
90
226
  content: string;
227
+ sha256?: string;
91
228
  }>;
92
229
  }>;
230
+ listSkillBundles(): Promise<Array<{
231
+ name: string;
232
+ version: string;
233
+ hash: string;
234
+ }>>;
93
235
  getSchema(): Promise<any>;
94
236
  private get;
95
237
  private post;
238
+ private put;
239
+ private patch;
96
240
  private delete;
241
+ private request;
97
242
  }
243
+ export type BrainArea = {
244
+ id: string;
245
+ name: string;
246
+ kind: "company" | "project";
247
+ role: "owner" | "editor" | "viewer";
248
+ canWrite: boolean;
249
+ canManage: boolean;
250
+ uri: string;
251
+ httpsUrl: string;
252
+ updatedAt: number;
253
+ };
254
+ export type BrainNote = {
255
+ id: string;
256
+ areaId: string;
257
+ areaName: string;
258
+ title: string;
259
+ markdown: string;
260
+ status: string;
261
+ version: number;
262
+ hash?: string;
263
+ sourceId?: string;
264
+ sourceUri?: string;
265
+ uri: string;
266
+ httpsUrl: string;
267
+ createdAt: number;
268
+ updatedAt: number;
269
+ };
270
+ export type BrainSource = {
271
+ id: string;
272
+ areaId: string;
273
+ areaName: string;
274
+ kind: "text" | "audio" | "file";
275
+ filename?: string;
276
+ mimeType?: string;
277
+ byteSize?: number;
278
+ sha256?: string;
279
+ scanState?: "pending" | "clean" | "quarantined" | "failed";
280
+ processingState: string;
281
+ rawText?: string;
282
+ extractedText?: string;
283
+ transcript?: string;
284
+ isMeeting: boolean;
285
+ derivedNoteUri?: string;
286
+ canDownload: boolean;
287
+ uri: string;
288
+ httpsUrl: string;
289
+ uploadedAt: number;
290
+ };
291
+ export type BrainResolvedResource = {
292
+ resourceType: "area";
293
+ area: BrainArea;
294
+ } | {
295
+ resourceType: "note";
296
+ note: BrainNote;
297
+ } | {
298
+ resourceType: "source";
299
+ source: BrainSource;
300
+ };
package/dist/client.js CHANGED
@@ -2,12 +2,30 @@
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";
11
+ /**
12
+ * Node's Buffer is a Uint8Array view over a possibly-larger, possibly-shared
13
+ * backing store, which is not assignable to BodyInit. Copy out the exact bytes.
14
+ */
15
+ export function toArrayBuffer(view) {
16
+ const out = new ArrayBuffer(view.byteLength);
17
+ new Uint8Array(out).set(view);
18
+ return out;
19
+ }
6
20
  export class ApiError extends Error {
7
21
  status;
8
- constructor(status, message) {
22
+ code;
23
+ requestId;
24
+ constructor(status, message, code, requestId) {
9
25
  super(message);
10
26
  this.status = status;
27
+ this.code = code;
28
+ this.requestId = requestId;
11
29
  this.name = "ApiError";
12
30
  }
13
31
  }
@@ -31,6 +49,145 @@ export class DayOfWeekClient {
31
49
  async revokeDevice(deviceId) {
32
50
  return this.delete(`/auth/devices/${deviceId}`);
33
51
  }
52
+ async revokeCurrentDevice() {
53
+ return this.delete("/auth/current-device");
54
+ }
55
+ // ── Shared brain ─────────────────────────────────────────────────────────
56
+ async brainBootstrap() {
57
+ return this.get("/brain/bootstrap");
58
+ }
59
+ async listBrainAreas() {
60
+ return this.get("/brain/areas");
61
+ }
62
+ async companyBrainStatus() {
63
+ return this.get("/brain/company-area");
64
+ }
65
+ async ensureCompanyBrain() {
66
+ return this.post("/brain/company-area", {});
67
+ }
68
+ async resolveBrain(uri) {
69
+ return this.get(`/brain/resolve?uri=${encodeURIComponent(uri)}`);
70
+ }
71
+ async getBrainNote(id) {
72
+ return this.get(`/brain/notes/${encodeURIComponent(id)}`);
73
+ }
74
+ async getBrainSource(id) {
75
+ return this.get(`/brain/sources/${encodeURIComponent(id)}`);
76
+ }
77
+ async uploadBrainSource(input) {
78
+ const info = statSync(input.path);
79
+ if (!info.isFile())
80
+ throw new Error("Upload path is not a regular file");
81
+ const sha256 = await hashFile(input.path);
82
+ const idempotencyKey = `up_${createHash("sha256")
83
+ .update(`${input.areaId}:${sha256}:${input.isMeeting}`)
84
+ .digest("base64url")}`;
85
+ const session = await this.post("/brain/source-uploads", {
86
+ areaId: input.areaId,
87
+ filename: basename(input.path),
88
+ mimeType: input.mimeType,
89
+ byteSize: info.size,
90
+ sha256,
91
+ isMeeting: input.isMeeting,
92
+ consentAcknowledged: input.consentAcknowledged,
93
+ idempotencyKey,
94
+ });
95
+ const upload = await fetch(session.uploadUrl, {
96
+ method: "POST",
97
+ headers: { "Content-Type": input.mimeType },
98
+ body: createReadStream(input.path),
99
+ duplex: "half",
100
+ });
101
+ if (!upload.ok)
102
+ throw new ApiError(upload.status, "Source byte upload failed", "network_error");
103
+ const stored = await upload.json();
104
+ if (!stored.storageId)
105
+ throw new Error("Upload did not return a storage ID");
106
+ const completed = await this.post(`/brain/source-uploads/${encodeURIComponent(session.uploadId)}/complete`, {
107
+ storageId: stored.storageId,
108
+ });
109
+ if (completed.sha256 !== sha256)
110
+ throw new Error("Server checksum does not match the uploaded file");
111
+ return completed;
112
+ }
113
+ async downloadBrainSource(input) {
114
+ if (!input.overwrite && existsSync(input.outputPath))
115
+ throw new Error("Output already exists; use --overwrite");
116
+ const response = await fetch(`${this.baseUrl}/brain/sources/${encodeURIComponent(input.sourceId)}/download`, {
117
+ headers: { Authorization: `Bearer ${this.token}` },
118
+ });
119
+ if (!response.ok)
120
+ throw await apiErrorFromResponse(response);
121
+ if (!response.body)
122
+ throw new Error("Download response had no body");
123
+ const expected = response.headers.get("x-dayofweek-sha256") ?? input.expectedSha256;
124
+ if (!expected || !/^[a-f0-9]{64}$/.test(expected))
125
+ throw new Error("Download response omitted its checksum");
126
+ const tempPath = join(dirname(input.outputPath), `.${basename(input.outputPath)}.dcli-${randomUUID()}.tmp`);
127
+ const hash = createHash("sha256");
128
+ let byteSize = 0;
129
+ const hasher = new Transform({
130
+ transform(chunk, _encoding, callback) {
131
+ hash.update(chunk);
132
+ byteSize += chunk.length;
133
+ callback(null, chunk);
134
+ },
135
+ });
136
+ try {
137
+ await pipeline(Readable.fromWeb(response.body), hasher, createWriteStream(tempPath, { flags: "wx", mode: 0o600 }));
138
+ const descriptor = openSync(tempPath, "r");
139
+ try {
140
+ fsyncSync(descriptor);
141
+ }
142
+ finally {
143
+ closeSync(descriptor);
144
+ }
145
+ const actual = hash.digest("hex");
146
+ if (actual !== expected || (input.expectedSha256 && actual !== input.expectedSha256)) {
147
+ throw new Error("Downloaded source checksum mismatch");
148
+ }
149
+ if (input.overwrite) {
150
+ renameSync(tempPath, input.outputPath);
151
+ }
152
+ else {
153
+ linkSync(tempPath, input.outputPath);
154
+ unlinkSync(tempPath);
155
+ }
156
+ return { outputPath: input.outputPath, byteSize, sha256: actual };
157
+ }
158
+ catch (error) {
159
+ if (existsSync(tempPath))
160
+ unlinkSync(tempPath);
161
+ throw error;
162
+ }
163
+ }
164
+ async searchBrain(query, options) {
165
+ return this.post("/brain/search", {
166
+ query,
167
+ areaId: options?.areaId,
168
+ limit: options?.limit,
169
+ });
170
+ }
171
+ async shareBrainNote(input) {
172
+ return this.post("/brain/notes", input);
173
+ }
174
+ async updateBrainNote(id, input) {
175
+ return this.patch(`/brain/notes/${encodeURIComponent(id)}`, input);
176
+ }
177
+ async archiveBrainNote(id, expectedVersion) {
178
+ return this.delete(`/brain/notes/${encodeURIComponent(id)}?expectedVersion=${expectedVersion}`);
179
+ }
180
+ async restoreBrainNote(id, expectedVersion) {
181
+ return this.post(`/brain/notes/${encodeURIComponent(id)}/restore`, { expectedVersion });
182
+ }
183
+ async listBrainAudit(areaId, options) {
184
+ const query = new URLSearchParams({ areaId });
185
+ if (options?.cursor)
186
+ query.set("cursor", options.cursor);
187
+ if (options?.limit)
188
+ query.set("limit", String(options.limit));
189
+ return this.get(`/brain/audit?${query}`);
190
+ }
34
191
  // ── Read ──────────────────────────────────────────────────────────────────
35
192
  async listEntities(opts) {
36
193
  const params = new URLSearchParams();
@@ -131,9 +288,94 @@ export class DayOfWeekClient {
131
288
  const qs = params.toString();
132
289
  return this.get(`/admin/proposals${qs ? `?${qs}` : ""}`);
133
290
  }
291
+ // ── Knowledge ─────────────────────────────────────────────────────────────
292
+ /**
293
+ * List the knowledge documents attached to an entity. `full` returns each
294
+ * document's whole content instead of an excerpt, which is what you want when
295
+ * mirroring an entity's sources into an external knowledge base.
296
+ */
297
+ async listKnowledge(opts) {
298
+ const params = new URLSearchParams({ entity: opts.entity });
299
+ if (opts.full)
300
+ params.set("full", "1");
301
+ if (opts.org)
302
+ params.set("org", opts.org);
303
+ return this.get(`/knowledge?${params.toString()}`);
304
+ }
305
+ async getKnowledge(documentId, org) {
306
+ const params = new URLSearchParams({ document: documentId });
307
+ if (org)
308
+ params.set("org", org);
309
+ return this.get(`/knowledge?${params.toString()}`);
310
+ }
311
+ /** Semantic search across knowledge documents. */
312
+ async searchKnowledge(opts) {
313
+ const params = new URLSearchParams({ q: opts.query });
314
+ if (opts.entity)
315
+ params.set("entity", opts.entity);
316
+ if (opts.types?.length)
317
+ params.set("types", opts.types.join(","));
318
+ if (opts.limit)
319
+ params.set("limit", String(opts.limit));
320
+ if (opts.allOrgs)
321
+ params.set("allOrgs", "1");
322
+ if (opts.org)
323
+ params.set("org", opts.org);
324
+ return this.get(`/knowledge?${params.toString()}`);
325
+ }
326
+ /**
327
+ * Add a markdown knowledge note.
328
+ *
329
+ * Without `direct` this submits a proposal for human review — the default,
330
+ * and the only option non-admin tokens have. With `direct: true` an admin
331
+ * token writes the document immediately, skipping review. Ask the operator
332
+ * before doing that; see references/admin.md in the served skill.
333
+ */
334
+ async addKnowledge(input) {
335
+ return this.post("/knowledge", input);
336
+ }
337
+ /**
338
+ * Attach a binary source (PDF, DOCX, XLSX, …) to an entity. Admin only.
339
+ *
340
+ * Three hops: ask for an upload URL, send the bytes straight to Convex
341
+ * storage, then register the resulting storageId. The bytes never pass
342
+ * through function arguments, so file size isn't bounded by an arg limit.
343
+ */
344
+ async attachKnowledgeFile(input) {
345
+ const { uploadUrl } = await this.post("/knowledge/file", {
346
+ entityId: input.entityId,
347
+ org: input.org,
348
+ });
349
+ const byteSize = input.data.byteLength;
350
+ const uploadRes = await fetch(uploadUrl, {
351
+ method: "POST",
352
+ headers: { "Content-Type": input.mimeType },
353
+ body: input.data,
354
+ });
355
+ if (!uploadRes.ok) {
356
+ throw new ApiError(uploadRes.status, `Upload to storage failed: ${uploadRes.status} ${uploadRes.statusText}`);
357
+ }
358
+ const uploaded = (await uploadRes.json());
359
+ if (!uploaded.storageId)
360
+ throw new Error("Storage upload returned no storageId");
361
+ return this.put("/knowledge/file", {
362
+ entityId: input.entityId,
363
+ org: input.org,
364
+ storageId: uploaded.storageId,
365
+ fileName: input.fileName,
366
+ mimeType: input.mimeType,
367
+ byteSize,
368
+ sourceType: input.sourceType,
369
+ sourceUrl: input.sourceUrl,
370
+ sourceDescription: input.sourceDescription,
371
+ });
372
+ }
134
373
  // ── Skill ─────────────────────────────────────────────────────────────────
135
- async getSkillBundle() {
136
- return this.get("/skill");
374
+ async getSkillBundle(name) {
375
+ return this.get(`/skill${name ? `?name=${encodeURIComponent(name)}` : ""}`);
376
+ }
377
+ async listSkillBundles() {
378
+ return this.get("/skill?list=1");
137
379
  }
138
380
  // ── Schema ────────────────────────────────────────────────────────────────
139
381
  async getSchema() {
@@ -141,55 +383,49 @@ export class DayOfWeekClient {
141
383
  }
142
384
  // ── HTTP helpers ──────────────────────────────────────────────────────────
143
385
  async get(path) {
144
- const res = await fetch(`${this.baseUrl}${path}`, {
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();
386
+ return this.request(path, { method: "GET" });
160
387
  }
161
388
  async post(path, body) {
389
+ return this.request(path, { method: "POST", body: JSON.stringify(body) });
390
+ }
391
+ async put(path, body) {
392
+ return this.request(path, { method: "PUT", body: JSON.stringify(body) });
393
+ }
394
+ async patch(path, body) {
395
+ return this.request(path, { method: "PATCH", body: JSON.stringify(body) });
396
+ }
397
+ async delete(path) {
398
+ return this.request(path, { method: "DELETE" });
399
+ }
400
+ async request(path, init) {
162
401
  const res = await fetch(`${this.baseUrl}${path}`, {
163
- method: "POST",
402
+ ...init,
164
403
  headers: {
165
404
  Authorization: `Bearer ${this.token}`,
166
- "Content-Type": "application/json",
405
+ ...(init.body ? { "Content-Type": "application/json" } : {}),
406
+ ...init.headers,
167
407
  },
168
- body: JSON.stringify(body),
169
408
  });
170
- if (!res.ok) {
171
- const text = await res.text();
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
- }
409
+ if (!res.ok)
410
+ throw await apiErrorFromResponse(res);
182
411
  return res.json();
183
412
  }
184
- async delete(path) {
185
- const res = await fetch(`${this.baseUrl}${path}`, {
186
- method: "DELETE",
187
- headers: { Authorization: `Bearer ${this.token}` },
188
- });
189
- if (!res.ok) {
190
- const text = await res.text();
191
- throw new ApiError(res.status, text);
192
- }
193
- return res.json();
413
+ }
414
+ async function hashFile(path) {
415
+ const hash = createHash("sha256");
416
+ for await (const chunk of createReadStream(path))
417
+ hash.update(chunk);
418
+ return hash.digest("hex");
419
+ }
420
+ async function apiErrorFromResponse(response) {
421
+ const body = await response.text();
422
+ try {
423
+ const json = JSON.parse(body);
424
+ if (typeof json.error === "string")
425
+ return new ApiError(response.status, json.error);
426
+ return new ApiError(response.status, json.error?.message ?? `Request failed (${response.status})`, json.error?.code, json.error?.requestId);
427
+ }
428
+ catch {
429
+ return new ApiError(response.status, `Request failed (${response.status})`);
194
430
  }
195
431
  }
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
- loadConfig().authToken;
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
- console.error("No auth token found.");
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;