@wirecat/cli-core 0.411.0 → 0.413.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/CHANGELOG.md +32 -0
- package/dist/cli/bot/command.js +3 -3
- package/dist/cli/bot/messages-sync.d.ts +7 -0
- package/dist/cli/bot/{store.js → messages-sync.js} +4 -5
- package/dist/cli/bot/port.d.ts +3 -3
- package/dist/cli/config-command.js +1 -0
- package/dist/cli/entry.d.ts +59 -0
- package/dist/cli/entry.js +136 -0
- package/dist/cli/first-run.d.ts +1 -1
- package/dist/cli/first-run.js +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/tasks-command.js +2 -2
- package/dist/cli/moved.js +1 -0
- package/dist/cli/settings.d.ts +2 -0
- package/dist/cli/settings.js +5 -1
- package/dist/domain/providers.d.ts +25 -3
- package/dist/domain/providers.js +89 -5
- package/dist/entities/index.d.ts +29 -1
- package/dist/entities/index.js +5 -1
- package/dist/entities/knowledge-lists.d.ts +28 -0
- package/dist/entities/knowledge-lists.js +29 -0
- package/dist/errors/reasons.js +16 -1
- package/dist/files/native-writer-lease.js +17 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/oauth/authorize.d.ts +21 -0
- package/dist/oauth/authorize.js +130 -0
- package/dist/oauth/index.d.ts +5 -0
- package/dist/oauth/index.js +4 -0
- package/dist/oauth/keeper.d.ts +41 -0
- package/dist/oauth/keeper.js +136 -0
- package/dist/oauth/provider.d.ts +54 -0
- package/dist/oauth/provider.js +1 -0
- package/dist/oauth/requests.d.ts +11 -0
- package/dist/oauth/requests.js +129 -0
- package/dist/oauth/storage.d.ts +48 -0
- package/dist/oauth/storage.js +197 -0
- package/dist/search/account-list.d.ts +5 -2
- package/dist/search/account-list.js +27 -11
- package/dist/search/query.js +3 -1
- package/dist/sends/permission-aliases.js +1 -0
- package/dist/services/documents/knowledge.d.ts +5 -3
- package/dist/services/documents/knowledge.js +4 -4
- package/dist/services/meeting-links.d.ts +3 -1
- package/dist/services/meeting-links.js +2 -2
- package/dist/services/messages-search.js +15 -3
- package/dist/services/project-gathering.d.ts +2 -0
- package/dist/services/project-gathering.js +18 -2
- package/dist/services/search-all.js +5 -2
- package/dist/store/accounts.d.ts +3 -0
- package/dist/store/accounts.js +2 -0
- package/dist/store/exports.d.ts +1 -0
- package/dist/store/exports.js +1 -0
- package/dist/store/index.d.ts +3 -1
- package/dist/store/index.js +2 -0
- package/dist/store/knowledge-query.d.ts +6 -0
- package/dist/store/knowledge-query.js +34 -0
- package/dist/store/providers.d.ts +7 -0
- package/dist/store/providers.js +5 -0
- package/dist/store/sqlite/knowledge.d.ts +5 -4
- package/dist/store/sqlite/knowledge.js +50 -17
- package/dist/store/sqlite/mail-rules.d.ts +2 -1
- package/dist/store/sqlite/mail-rules.js +12 -2
- package/dist/store/sqlite/notes.d.ts +11 -5
- package/dist/store/sqlite/notes.js +9 -2
- package/dist/store/sqlite/open-store.js +2 -0
- package/dist/store/sqlite/providers.d.ts +12 -0
- package/dist/store/sqlite/providers.js +45 -0
- package/dist/store/sqlite/tags.d.ts +1 -1
- package/dist/store/sqlite/tags.js +3 -3
- package/package.json +5 -1
- package/parity.json +31 -35
- package/dist/cli/bot/store.d.ts +0 -7
package/dist/entities/index.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { imageEmbedding } from "./image-embedding.js";
|
|
|
39
39
|
import { inviteLink } from "./invite-link.js";
|
|
40
40
|
import { job } from "./job.js";
|
|
41
41
|
import { joinRequest } from "./join-request.js";
|
|
42
|
+
import { organization, project, reminder } from "./knowledge-lists.js";
|
|
42
43
|
import { botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, callRecord, scheduledJob, stickerInventory } from "./list-results.js";
|
|
43
44
|
import { messageEvent, serveSummary, watchedMessage, watchItem } from "./listener.js";
|
|
44
45
|
import { mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer } from "./media-archive.js";
|
|
@@ -85,7 +86,7 @@ import { topic } from "./topic.js";
|
|
|
85
86
|
* subset of its fields, never a renamed one. Commands move onto these one by one; until then, a schema
|
|
86
87
|
* is the target, not a description of today's output.
|
|
87
88
|
*/
|
|
88
|
-
export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactProfile, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, person, place, poll, pollChange, pollVote, ranking, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, removedModelKey, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, tagChange, tagging, task, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, trackedChat, trackedChatHistory, watchedMessage, watchItem, };
|
|
89
|
+
export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactProfile, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, organization, person, place, poll, pollChange, pollVote, project, ranking, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, reminder, removedModelKey, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, tagChange, tagging, task, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, trackedChat, trackedChatHistory, watchedMessage, watchItem, };
|
|
89
90
|
export declare const ENTITIES: {
|
|
90
91
|
readonly meeting_ingestion: v.StrictObjectSchema<{
|
|
91
92
|
readonly meetings: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
@@ -4742,6 +4743,33 @@ export declare const ENTITIES: {
|
|
|
4742
4743
|
readonly account: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
4743
4744
|
readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4744
4745
|
}, undefined>;
|
|
4746
|
+
readonly organization: v.StrictObjectSchema<{
|
|
4747
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
4748
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4749
|
+
readonly kind: v.PicklistSchema<["company", "team", "family", "community"], undefined>;
|
|
4750
|
+
readonly name: v.StringSchema<undefined>;
|
|
4751
|
+
readonly scope: v.PicklistSchema<["personal", "work"], undefined>;
|
|
4752
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
4753
|
+
}, undefined>;
|
|
4754
|
+
readonly project: v.StrictObjectSchema<{
|
|
4755
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
4756
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4757
|
+
readonly key: v.StringSchema<undefined>;
|
|
4758
|
+
readonly name: v.StringSchema<undefined>;
|
|
4759
|
+
readonly type: v.PicklistSchema<["work", "client", "personal", "oss", "other"], undefined>;
|
|
4760
|
+
readonly organization_id: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
4761
|
+
readonly scope: v.PicklistSchema<["personal", "work"], undefined>;
|
|
4762
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
4763
|
+
}, undefined>;
|
|
4764
|
+
readonly reminder: v.StrictObjectSchema<{
|
|
4765
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
4766
|
+
readonly task: v.StringSchema<undefined>;
|
|
4767
|
+
readonly due_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
4768
|
+
readonly timezone: v.StringSchema<undefined>;
|
|
4769
|
+
readonly state: v.PicklistSchema<["pending", "leased", "delivered", "cancelled"], undefined>;
|
|
4770
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
4771
|
+
readonly receipt: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4772
|
+
}, undefined>;
|
|
4745
4773
|
readonly bot_profile: v.StrictObjectSchema<{
|
|
4746
4774
|
readonly name: v.StringSchema<undefined>;
|
|
4747
4775
|
readonly token: v.StringSchema<undefined>;
|
package/dist/entities/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import { imageEmbedding } from "./image-embedding.js";
|
|
|
41
41
|
import { inviteLink } from "./invite-link.js";
|
|
42
42
|
import { job } from "./job.js";
|
|
43
43
|
import { joinRequest } from "./join-request.js";
|
|
44
|
+
import { organization, project, reminder } from "./knowledge-lists.js";
|
|
44
45
|
import { botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, callRecord, scheduledJob, stickerInventory, } from "./list-results.js";
|
|
45
46
|
import { messageEvent, serveSummary, watchedMessage, watchItem } from "./listener.js";
|
|
46
47
|
import { mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer } from "./media-archive.js";
|
|
@@ -91,7 +92,7 @@ import { topic } from "./topic.js";
|
|
|
91
92
|
* subset of its fields, never a renamed one. Commands move onto these one by one; until then, a schema
|
|
92
93
|
* is the target, not a description of today's output.
|
|
93
94
|
*/
|
|
94
|
-
export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactProfile, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, person, place, poll, pollChange, pollVote, ranking, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, removedModelKey, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, tagChange, tagging, task, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, trackedChat, trackedChatHistory, watchedMessage, watchItem, };
|
|
95
|
+
export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactProfile, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, organization, person, place, poll, pollChange, pollVote, project, ranking, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, reminder, removedModelKey, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, tagChange, tagging, task, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, trackedChat, trackedChatHistory, watchedMessage, watchItem, };
|
|
95
96
|
export const ENTITIES = {
|
|
96
97
|
sync_progress: syncProgress,
|
|
97
98
|
...MEETING_RESULT_ENTITIES,
|
|
@@ -167,6 +168,9 @@ export const ENTITIES = {
|
|
|
167
168
|
account_privacy: accountPrivacy,
|
|
168
169
|
account_privacy_change: accountPrivacyChange,
|
|
169
170
|
account_profile: accountProfile,
|
|
171
|
+
organization,
|
|
172
|
+
project,
|
|
173
|
+
reminder,
|
|
170
174
|
bot_profile: botProfile,
|
|
171
175
|
bot_template: botTemplate,
|
|
172
176
|
bot_seen_chat: botSeenChat,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
export declare const organization: v.StrictObjectSchema<{
|
|
3
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
4
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
5
|
+
readonly kind: v.PicklistSchema<["company", "team", "family", "community"], undefined>;
|
|
6
|
+
readonly name: v.StringSchema<undefined>;
|
|
7
|
+
readonly scope: v.PicklistSchema<["personal", "work"], undefined>;
|
|
8
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
9
|
+
}, undefined>;
|
|
10
|
+
export declare const project: v.StrictObjectSchema<{
|
|
11
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
12
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
13
|
+
readonly key: v.StringSchema<undefined>;
|
|
14
|
+
readonly name: v.StringSchema<undefined>;
|
|
15
|
+
readonly type: v.PicklistSchema<["work", "client", "personal", "oss", "other"], undefined>;
|
|
16
|
+
readonly organization_id: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
17
|
+
readonly scope: v.PicklistSchema<["personal", "work"], undefined>;
|
|
18
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
19
|
+
}, undefined>;
|
|
20
|
+
export declare const reminder: v.StrictObjectSchema<{
|
|
21
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
22
|
+
readonly task: v.StringSchema<undefined>;
|
|
23
|
+
readonly due_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
24
|
+
readonly timezone: v.StringSchema<undefined>;
|
|
25
|
+
readonly state: v.PicklistSchema<["pending", "leased", "delivered", "cancelled"], undefined>;
|
|
26
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
27
|
+
readonly receipt: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
28
|
+
}, undefined>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import { id, reference, text, time } from "./fields.js";
|
|
3
|
+
export const organization = v.strictObject({
|
|
4
|
+
id,
|
|
5
|
+
ref: reference,
|
|
6
|
+
kind: v.picklist(["company", "team", "family", "community"]),
|
|
7
|
+
name: text,
|
|
8
|
+
scope: v.picklist(["personal", "work"]),
|
|
9
|
+
created_at: time,
|
|
10
|
+
});
|
|
11
|
+
export const project = v.strictObject({
|
|
12
|
+
id,
|
|
13
|
+
ref: reference,
|
|
14
|
+
key: text,
|
|
15
|
+
name: text,
|
|
16
|
+
type: v.picklist(["work", "client", "personal", "oss", "other"]),
|
|
17
|
+
organization_id: v.nullable(id),
|
|
18
|
+
scope: v.picklist(["personal", "work"]),
|
|
19
|
+
created_at: time,
|
|
20
|
+
});
|
|
21
|
+
export const reminder = v.strictObject({
|
|
22
|
+
id,
|
|
23
|
+
task: text,
|
|
24
|
+
due_at: time,
|
|
25
|
+
timezone: text,
|
|
26
|
+
state: v.picklist(["pending", "leased", "delivered", "cancelled"]),
|
|
27
|
+
revision: v.number(),
|
|
28
|
+
receipt: v.nullable(text),
|
|
29
|
+
});
|
package/dist/errors/reasons.js
CHANGED
|
@@ -15,6 +15,21 @@ const QUERY = [
|
|
|
15
15
|
["query.invalid-topic", "a topic in the query is not valid"],
|
|
16
16
|
["query.topic-scope", "a topic needs its chat in the query"],
|
|
17
17
|
].map(([reason, message]) => validation(reason, message));
|
|
18
|
+
const SIGN_IN = [
|
|
19
|
+
["oauth.declined", "authentication_error", "the provider's consent was declined"],
|
|
20
|
+
["oauth.timeout", "timeout", "the sign-in was not finished in time"],
|
|
21
|
+
["oauth.mismatch", "authentication_error", "the pasted sign-in result belongs to another sign-in"],
|
|
22
|
+
["oauth.missing", "authentication_error", "this profile is not signed in"],
|
|
23
|
+
["oauth.locked", "provider_unavailable", "another process holds this sign-in's lock"],
|
|
24
|
+
["oauth.no-response", "outcome_unknown", "the token request did not finish"],
|
|
25
|
+
["oauth.unavailable", "provider_unavailable", "the provider's token endpoint is unavailable"],
|
|
26
|
+
["oauth.expired", "authentication_error", "the provider refused the saved sign-in; sign in again"],
|
|
27
|
+
["oauth.invalid-response", "invalid_response", "the provider's token response could not be used"],
|
|
28
|
+
["oauth.failed", "authentication_error", "the provider returned no offline refresh token"],
|
|
29
|
+
["oauth.revoke-failed", "outcome_unknown", "the provider did not confirm the revoke"],
|
|
30
|
+
["oauth.invalid", "authentication_error", "the saved sign-in cannot be read"],
|
|
31
|
+
["oauth.not-saved", "configuration_error", "the sign-in cannot be saved or read where it is kept"],
|
|
32
|
+
];
|
|
18
33
|
const OTHERS = [
|
|
19
34
|
["model.offline", "permission_error", "downloading a model needs the network; --offline was requested"],
|
|
20
35
|
[
|
|
@@ -105,7 +120,7 @@ export const INPUT_FIELD_REASONS = defineReasons([
|
|
|
105
120
|
export const REASONS = defineReasons([
|
|
106
121
|
...QUERY_REASONS,
|
|
107
122
|
...INPUT_FIELD_REASONS,
|
|
108
|
-
...[...QUERY, ...OTHERS].map(([reason, code, message]) => ({
|
|
123
|
+
...[...QUERY, ...SIGN_IN, ...OTHERS].map(([reason, code, message]) => ({
|
|
109
124
|
reason,
|
|
110
125
|
code,
|
|
111
126
|
message,
|
|
@@ -22,15 +22,26 @@ export const nativeWriterLeaseStore = (layout) => {
|
|
|
22
22
|
const root = await realpath(destination);
|
|
23
23
|
signal?.throwIfAborted();
|
|
24
24
|
const folder = join(root, markerDirectory);
|
|
25
|
-
await mkdir(folder, { recursive: true });
|
|
26
|
-
if ((await lstat(folder)).isSymbolicLink())
|
|
27
|
-
throw new CliError("validation_error", "archive lock directory must not be a symlink");
|
|
28
|
-
await inspection.recover(root, { apply: true, signal });
|
|
29
|
-
signal?.throwIfAborted();
|
|
30
25
|
const owner = { version: 1, pid: process.pid, host: hostname(), nonce: randomUUID() };
|
|
31
26
|
const name = `${owner.pid}-${owner.nonce}.json`;
|
|
32
27
|
const path = join(folder, name);
|
|
33
|
-
|
|
28
|
+
// Another writer removes the folder once it is empty, so it can vanish before our marker is in it.
|
|
29
|
+
const marker = async (attempt = 1) => {
|
|
30
|
+
await mkdir(folder, { recursive: true });
|
|
31
|
+
try {
|
|
32
|
+
if ((await lstat(folder)).isSymbolicLink())
|
|
33
|
+
throw new CliError("validation_error", "archive lock directory must not be a symlink");
|
|
34
|
+
await inspection.recover(root, { apply: true, signal });
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
return await open(path, "wx", 0o600);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (attempt < 5 && missingLeaseFile(error))
|
|
40
|
+
return marker(attempt + 1);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const file = await marker();
|
|
34
45
|
let retained = false;
|
|
35
46
|
let identity;
|
|
36
47
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { parseMarkdownSpans } from "./domain/markdown-spans.js";
|
|
|
8
8
|
export type { MessageLink, MessagePermalink } from "./domain/message-link.js";
|
|
9
9
|
export type * from "./domain/models.js";
|
|
10
10
|
export { countedPoll } from "./domain/polls.js";
|
|
11
|
+
export { isKind, KINDS, type Kind, kindIn, kindOf, PROVIDERS, type ProviderEntry, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
|
|
11
12
|
export type { RankingGraphEvidence, RankingGraphLink } from "./domain/rankings-graph.js";
|
|
12
13
|
export type { RankingComponent, RankingInput, RankingMeasure, RankingOptions, RankingTarget, RankingWeights, ScorePreset, } from "./domain/rankings-options.js";
|
|
13
14
|
export { rankingOptions } from "./domain/rankings-options.js";
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { formatLocator, isLocator, parseLocator } from "./domain/locator.js";
|
|
|
5
5
|
export { parseMarkdown } from "./domain/markdown.js";
|
|
6
6
|
export { parseMarkdownSpans } from "./domain/markdown-spans.js";
|
|
7
7
|
export { countedPoll } from "./domain/polls.js";
|
|
8
|
+
export { isKind, KINDS, kindIn, kindOf, PROVIDERS, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
|
|
8
9
|
export { rankingOptions } from "./domain/rankings-options.js";
|
|
9
10
|
export { canonicalReference, formatReference, parseReference } from "./domain/references.js";
|
|
10
11
|
export { normalizeTag, TAG_TYPES } from "./domain/tags.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { OAuthProvider } from "./provider.js";
|
|
2
|
+
export interface AuthorizeOptions {
|
|
3
|
+
provider: OAuthProvider;
|
|
4
|
+
scopes: readonly string[];
|
|
5
|
+
openBrowser: (url: string) => Promise<void>;
|
|
6
|
+
showUrl?: (url: string) => void;
|
|
7
|
+
/** The redirected URL, or its query, pasted when the browser cannot reach this machine's loopback. */
|
|
8
|
+
readPaste?: (signal: AbortSignal) => Promise<string>;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
export interface Authorization {
|
|
13
|
+
code: string;
|
|
14
|
+
redirectUri: string;
|
|
15
|
+
codeVerifier: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Authorization code with PKCE S256 for an installed app: a loopback listener on 127.0.0.1, a `state` check, a
|
|
19
|
+
* timeout, cancel and a paste fallback. Answers the code without exchanging it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const authorize: (options: AuthorizeOptions) => Promise<Authorization>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
5
|
+
const pkce = () => {
|
|
6
|
+
const codeVerifier = randomBytes(48).toString("base64url");
|
|
7
|
+
return { codeVerifier, codeChallenge: createHash("sha256").update(codeVerifier).digest("base64url") };
|
|
8
|
+
};
|
|
9
|
+
const queryOf = (text) => {
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
const start = trimmed.indexOf("?");
|
|
12
|
+
return new URLSearchParams(start >= 0 ? trimmed.slice(start + 1).split("#")[0] : trimmed);
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Authorization code with PKCE S256 for an installed app: a loopback listener on 127.0.0.1, a `state` check, a
|
|
16
|
+
* timeout, cancel and a paste fallback. Answers the code without exchanging it.
|
|
17
|
+
*/
|
|
18
|
+
export const authorize = async (options) => {
|
|
19
|
+
const { provider } = options;
|
|
20
|
+
const cancelled = () => isCliError(options.signal?.reason)
|
|
21
|
+
? options.signal.reason
|
|
22
|
+
: new CliError("cancelled", `${provider.name} sign-in was cancelled`);
|
|
23
|
+
if (options.signal?.aborted)
|
|
24
|
+
throw cancelled();
|
|
25
|
+
const { codeVerifier, codeChallenge } = pkce();
|
|
26
|
+
const paste = new AbortController();
|
|
27
|
+
let state = "";
|
|
28
|
+
let settled = false;
|
|
29
|
+
let accept = () => { };
|
|
30
|
+
let fail = () => { };
|
|
31
|
+
const code = new Promise((resolve, reject) => {
|
|
32
|
+
accept = (value) => {
|
|
33
|
+
settled = true;
|
|
34
|
+
resolve(value);
|
|
35
|
+
};
|
|
36
|
+
fail = (error) => {
|
|
37
|
+
settled = true;
|
|
38
|
+
reject(error);
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
void code.catch(() => { });
|
|
42
|
+
const settle = (query) => {
|
|
43
|
+
const value = query.get("code");
|
|
44
|
+
if (value)
|
|
45
|
+
accept(value);
|
|
46
|
+
else
|
|
47
|
+
fail(new CliError("authentication_error", `${provider.name} sign-in was declined`, { reason: "oauth.declined" }));
|
|
48
|
+
};
|
|
49
|
+
const server = createServer((request, response) => {
|
|
50
|
+
let url;
|
|
51
|
+
try {
|
|
52
|
+
url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
response.writeHead(400).end();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (request.method !== "GET" || url.pathname !== "/callback") {
|
|
59
|
+
response.writeHead(404).end();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (settled || !state || url.searchParams.get("state") !== state) {
|
|
63
|
+
response.writeHead(400).end("This sign-in link does not belong to the waiting sign-in");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!url.searchParams.get("code") && !url.searchParams.get("error")) {
|
|
67
|
+
response.writeHead(400).end("Missing sign-in result");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
response.end("Sign-in received; you can close this tab");
|
|
71
|
+
settle(url.searchParams);
|
|
72
|
+
});
|
|
73
|
+
const onAbort = () => fail(cancelled());
|
|
74
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
75
|
+
const timer = setTimeout(() => fail(new CliError("timeout", `${provider.name} sign-in timed out`, { reason: "oauth.timeout" })), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
76
|
+
try {
|
|
77
|
+
await new Promise((resolve, reject) => {
|
|
78
|
+
server.once("error", reject);
|
|
79
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
80
|
+
});
|
|
81
|
+
const address = server.address();
|
|
82
|
+
if (!address || typeof address === "string")
|
|
83
|
+
throw new CliError("configuration_error", "could not start the local sign-in listener");
|
|
84
|
+
const random = randomBytes(32).toString("base64url");
|
|
85
|
+
const relay = provider.redirect.kind === "relay";
|
|
86
|
+
state = relay ? `${address.port}.${random}` : random;
|
|
87
|
+
const redirectUri = provider.redirect.kind === "relay" ? provider.redirect.url : `http://127.0.0.1:${address.port}/callback`;
|
|
88
|
+
const url = new URL(provider.authorizeUrl);
|
|
89
|
+
for (const [key, value] of Object.entries({
|
|
90
|
+
...provider.authorizeParams,
|
|
91
|
+
response_type: "code",
|
|
92
|
+
client_id: provider.client.clientId,
|
|
93
|
+
redirect_uri: redirectUri,
|
|
94
|
+
...(options.scopes.length ? { scope: options.scopes.join(" ") } : {}),
|
|
95
|
+
state,
|
|
96
|
+
code_challenge: codeChallenge,
|
|
97
|
+
code_challenge_method: "S256",
|
|
98
|
+
}))
|
|
99
|
+
url.searchParams.set(key, value);
|
|
100
|
+
options.showUrl?.(url.toString());
|
|
101
|
+
options
|
|
102
|
+
.readPaste?.(paste.signal)
|
|
103
|
+
.then((text) => {
|
|
104
|
+
if (settled)
|
|
105
|
+
return;
|
|
106
|
+
const query = queryOf(text);
|
|
107
|
+
if (query.get("state") !== state || (!query.get("code") && !query.get("error")))
|
|
108
|
+
fail(new CliError("authentication_error", "the pasted sign-in result does not match this sign-in", {
|
|
109
|
+
reason: "oauth.mismatch",
|
|
110
|
+
}));
|
|
111
|
+
else
|
|
112
|
+
settle(query);
|
|
113
|
+
})
|
|
114
|
+
.catch(() => { });
|
|
115
|
+
void Promise.resolve()
|
|
116
|
+
.then(() => options.openBrowser(url.toString()))
|
|
117
|
+
.catch(() => {
|
|
118
|
+
if (!options.readPaste && !options.showUrl)
|
|
119
|
+
fail(new CliError("configuration_error", "could not open the browser, and no sign-in link can be shown"));
|
|
120
|
+
});
|
|
121
|
+
return { code: await code, redirectUri, codeVerifier };
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
126
|
+
paste.abort();
|
|
127
|
+
server.closeAllConnections();
|
|
128
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { type Authorization, type AuthorizeOptions, authorize } from "./authorize.js";
|
|
2
|
+
export { type SignInOptions, type TokenKeeper, type TokenKeeperOptions, tokenKeeper } from "./keeper.js";
|
|
3
|
+
export type { ExchangeContext, OAuthClient, OAuthProvider, RequestContext, TokenSet } from "./provider.js";
|
|
4
|
+
export { requestTokens, revokeToken, TOKEN_LOG_ALLOW, TOKEN_LOG_EVENT } from "./requests.js";
|
|
5
|
+
export { type CredentialStorageOptions, credentialStorage, driveSessionCodec, type EncodeInput, oauthAccount, type StoredTokens, standardCodec, type TokenCodec, type TokenStorage, zoomTokenCodec, } from "./storage.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { authorize } from "./authorize.js";
|
|
2
|
+
export { tokenKeeper } from "./keeper.js";
|
|
3
|
+
export { requestTokens, revokeToken, TOKEN_LOG_ALLOW, TOKEN_LOG_EVENT } from "./requests.js";
|
|
4
|
+
export { credentialStorage, driveSessionCodec, oauthAccount, standardCodec, zoomTokenCodec, } from "./storage.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type CredentialSource, type Logger } from "@wirecat/cli-base";
|
|
2
|
+
import { type AuthorizeOptions } from "./authorize.js";
|
|
3
|
+
import type { OAuthProvider, TokenSet } from "./provider.js";
|
|
4
|
+
import type { TokenStorage } from "./storage.js";
|
|
5
|
+
export interface TokenKeeperOptions {
|
|
6
|
+
provider: OAuthProvider;
|
|
7
|
+
storage: TokenStorage;
|
|
8
|
+
/** The CLI's config directory: the keyring entry varies with it, so the lock must too. */
|
|
9
|
+
lockDir: string;
|
|
10
|
+
fetch: typeof fetch;
|
|
11
|
+
/** `drive personal session start`: named in every failure that needs a new sign-in. */
|
|
12
|
+
signInCommand: string;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
log?: Logger;
|
|
16
|
+
/** Longer than one token request's 30 s limit, so a live refresher finishes first. Real time, not `now`. */
|
|
17
|
+
lockWaitMs?: number;
|
|
18
|
+
lockRetryMs?: number;
|
|
19
|
+
/** An access token expiring later than this from now is used as is. */
|
|
20
|
+
freshForMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export type SignInOptions = Omit<AuthorizeOptions, "provider" | "signal"> & {
|
|
23
|
+
/** Runs before anything is saved; it may refuse the account or add fields to the entry (drive's `principal`). */
|
|
24
|
+
accept?: (tokens: TokenSet) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
|
|
25
|
+
};
|
|
26
|
+
export interface TokenKeeper {
|
|
27
|
+
accessToken(): Promise<string>;
|
|
28
|
+
tokens(): Promise<TokenSet>;
|
|
29
|
+
signIn(options: SignInOptions): Promise<{
|
|
30
|
+
scopes: string[];
|
|
31
|
+
}>;
|
|
32
|
+
/** Saves tokens a CLI got by a flow of its own, under the same lock. */
|
|
33
|
+
save(tokens: TokenSet, extra?: Record<string, unknown>): Promise<void>;
|
|
34
|
+
signOut(options?: {
|
|
35
|
+
revoke?: boolean;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
revoked: boolean;
|
|
38
|
+
removed: CredentialSource[];
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
export declare const tokenKeeper: (options: TokenKeeperOptions) => TokenKeeper;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
5
|
+
import { acquireLock } from "../files/index.js";
|
|
6
|
+
import { authorize } from "./authorize.js";
|
|
7
|
+
import { requestTokens, revokeToken, TOKEN_LOG_EVENT } from "./requests.js";
|
|
8
|
+
const inFlight = new Map();
|
|
9
|
+
export const tokenKeeper = (options) => {
|
|
10
|
+
const { provider, storage, signal, log } = options;
|
|
11
|
+
const now = options.now ?? Date.now;
|
|
12
|
+
const fresh = (tokens) => Boolean(tokens.accessToken) &&
|
|
13
|
+
tokens.expiresAt !== undefined &&
|
|
14
|
+
tokens.expiresAt > now() + (options.freshForMs ?? 60_000);
|
|
15
|
+
const lockPath = join(options.lockDir, `oauth-${createHash("sha256").update(storage.key).digest("hex").slice(0, 16)}.lock`);
|
|
16
|
+
const clientOf = (stored) => {
|
|
17
|
+
const saved = stored?.client;
|
|
18
|
+
if (!saved || saved.clientSecret || saved.clientId !== provider.client.clientId)
|
|
19
|
+
return saved ?? provider.client;
|
|
20
|
+
return provider.client;
|
|
21
|
+
};
|
|
22
|
+
const context = (client, previous) => ({
|
|
23
|
+
fetch: options.fetch,
|
|
24
|
+
client,
|
|
25
|
+
now,
|
|
26
|
+
signInCommand: options.signInCommand,
|
|
27
|
+
...(signal ? { signal } : {}),
|
|
28
|
+
...(log ? { log } : {}),
|
|
29
|
+
...(previous ? { previous } : {}),
|
|
30
|
+
});
|
|
31
|
+
const exchange = (params, ctx) => provider.exchange ? provider.exchange(params, ctx) : requestTokens(provider, params, ctx);
|
|
32
|
+
const notSignedIn = () => new CliError("authentication_error", `not signed in to ${provider.name}; run ${options.signInCommand}`, {
|
|
33
|
+
reason: "oauth.missing",
|
|
34
|
+
});
|
|
35
|
+
const busy = (error) => isCliError(error) && error.details.reason === "file.locked";
|
|
36
|
+
const acquire = async () => {
|
|
37
|
+
const started = performance.now();
|
|
38
|
+
let waiting = false;
|
|
39
|
+
for (;;) {
|
|
40
|
+
try {
|
|
41
|
+
return await acquireLock(lockPath, signal ? { signal } : {});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (!busy(error))
|
|
45
|
+
throw error;
|
|
46
|
+
if (!waiting) {
|
|
47
|
+
waiting = true;
|
|
48
|
+
log?.info({
|
|
49
|
+
event: TOKEN_LOG_EVENT,
|
|
50
|
+
provider: provider.id,
|
|
51
|
+
operation: "lock",
|
|
52
|
+
outcome: "waiting",
|
|
53
|
+
pid: process.pid,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (performance.now() - started > (options.lockWaitMs ?? 45_000))
|
|
57
|
+
throw new CliError("provider_unavailable", `another process holds this ${provider.name} sign-in; retry`, {
|
|
58
|
+
reason: "oauth.locked",
|
|
59
|
+
retryable: true,
|
|
60
|
+
});
|
|
61
|
+
await delay(options.lockRetryMs ?? 50, undefined, signal ? { signal } : {}).catch(() => {
|
|
62
|
+
throw new CliError("cancelled", `${provider.name} sign-in was cancelled`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const locked = async (work) => {
|
|
68
|
+
const lock = await acquire();
|
|
69
|
+
try {
|
|
70
|
+
return await work();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await lock.release().catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const shared = (work) => {
|
|
77
|
+
const running = inFlight.get(lockPath);
|
|
78
|
+
if (running)
|
|
79
|
+
return running;
|
|
80
|
+
const started = locked(work).finally(() => inFlight.delete(lockPath));
|
|
81
|
+
inFlight.set(lockPath, started);
|
|
82
|
+
return started;
|
|
83
|
+
};
|
|
84
|
+
const tokens = async () => {
|
|
85
|
+
const seen = storage.read();
|
|
86
|
+
if (!seen)
|
|
87
|
+
throw notSignedIn();
|
|
88
|
+
if (fresh(seen.tokens))
|
|
89
|
+
return seen.tokens;
|
|
90
|
+
return shared(async () => {
|
|
91
|
+
const current = storage.read();
|
|
92
|
+
if (!current)
|
|
93
|
+
throw notSignedIn();
|
|
94
|
+
// Another process refreshed while this one waited for the lock: its result is the one to use.
|
|
95
|
+
if (fresh(current.tokens) || current.tokens.refreshToken !== seen.tokens.refreshToken)
|
|
96
|
+
return current.tokens;
|
|
97
|
+
const client = clientOf(current);
|
|
98
|
+
const refreshed = await exchange({ grant_type: "refresh_token", refresh_token: current.tokens.refreshToken }, context(client, current.tokens));
|
|
99
|
+
storage.write({ tokens: refreshed, client });
|
|
100
|
+
return refreshed;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
tokens,
|
|
105
|
+
accessToken: async () => {
|
|
106
|
+
const current = await tokens();
|
|
107
|
+
if (!current.accessToken)
|
|
108
|
+
throw notSignedIn();
|
|
109
|
+
return current.accessToken;
|
|
110
|
+
},
|
|
111
|
+
signIn: async ({ accept, ...authorizeOptions }) => {
|
|
112
|
+
storage.probe();
|
|
113
|
+
const authorization = await authorize({ ...authorizeOptions, provider, ...(signal ? { signal } : {}) });
|
|
114
|
+
const client = provider.client;
|
|
115
|
+
const received = await exchange({
|
|
116
|
+
grant_type: "authorization_code",
|
|
117
|
+
code: authorization.code,
|
|
118
|
+
redirect_uri: authorization.redirectUri,
|
|
119
|
+
code_verifier: authorization.codeVerifier,
|
|
120
|
+
}, context(client));
|
|
121
|
+
const extra = (await accept?.(received)) ?? undefined;
|
|
122
|
+
await locked(async () => storage.write({ tokens: received, client, ...(extra ? { extra } : {}) }));
|
|
123
|
+
return { scopes: received.scopes };
|
|
124
|
+
},
|
|
125
|
+
save: (saved, extra) => locked(async () => storage.write({ tokens: saved, client: clientOf(storage.read()), ...(extra ? { extra } : {}) })),
|
|
126
|
+
signOut: ({ revoke = false } = {}) => locked(async () => {
|
|
127
|
+
const current = storage.read();
|
|
128
|
+
let revoked = false;
|
|
129
|
+
if (revoke && current) {
|
|
130
|
+
await revokeToken(provider, current.tokens, context(clientOf(current)));
|
|
131
|
+
revoked = true;
|
|
132
|
+
}
|
|
133
|
+
return { revoked, removed: storage.remove() };
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Logger } from "@wirecat/cli-base";
|
|
2
|
+
/** The app identity a provider issued; an installed app's secret is public, not account access. */
|
|
3
|
+
export interface OAuthClient {
|
|
4
|
+
clientId: string;
|
|
5
|
+
clientSecret?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TokenSet {
|
|
8
|
+
accessToken?: string;
|
|
9
|
+
refreshToken: string;
|
|
10
|
+
/** UTC milliseconds; unknown means "refresh before use". */
|
|
11
|
+
expiresAt?: number;
|
|
12
|
+
scopes: string[];
|
|
13
|
+
tokenType?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RequestContext {
|
|
16
|
+
fetch: typeof fetch;
|
|
17
|
+
client: OAuthClient;
|
|
18
|
+
now: () => number;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
log?: Logger;
|
|
21
|
+
/** The command that signs in again, named in every failure that needs it. */
|
|
22
|
+
signInCommand: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ExchangeContext extends RequestContext {
|
|
25
|
+
/** The tokens being refreshed; absent for the first exchange. */
|
|
26
|
+
previous?: TokenSet;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A provider as a CLI declares it. cli-core holds no provider's URLs or ids; every step may be replaced, and the
|
|
30
|
+
* keeper still holds the lock and the storage around it.
|
|
31
|
+
*/
|
|
32
|
+
export interface OAuthProvider {
|
|
33
|
+
/** A short word, part of new keyring account names: `google`, `zoom`. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** The name people know, used in messages: `Google`. */
|
|
36
|
+
name: string;
|
|
37
|
+
authorizeUrl: string;
|
|
38
|
+
tokenUrl: string;
|
|
39
|
+
revokeUrl?: string;
|
|
40
|
+
client: OAuthClient;
|
|
41
|
+
/**
|
|
42
|
+
* `loopback` sends the browser to `http://127.0.0.1:<port>/callback`. `relay` sends it to a fixed page that
|
|
43
|
+
* forwards to the loopback port, which it reads from `state` (`<port>.<random>`).
|
|
44
|
+
*/
|
|
45
|
+
redirect: {
|
|
46
|
+
kind: "loopback";
|
|
47
|
+
} | {
|
|
48
|
+
kind: "relay";
|
|
49
|
+
url: string;
|
|
50
|
+
};
|
|
51
|
+
authorizeParams?: Record<string, string>;
|
|
52
|
+
exchange?: (params: Record<string, string>, context: ExchangeContext) => Promise<TokenSet>;
|
|
53
|
+
revoke?: (tokens: TokenSet, context: RequestContext) => Promise<void>;
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|