@stina/extension-api 1.6.0 → 1.9.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/{chunk-3Q3YXWOH.js → chunk-S3YP4QPF.js} +1 -1
- package/dist/{chunk-3Q3YXWOH.js.map → chunk-S3YP4QPF.js.map} +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.js +1 -1
- package/dist/runtime.cjs +24 -6
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +25 -7
- package/dist/runtime.js.map +1 -1
- package/dist/schemas/index.cjs +36 -2
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +92 -6
- package/dist/schemas/index.d.ts +92 -6
- package/dist/schemas/index.js +32 -2
- package/dist/schemas/index.js.map +1 -1
- package/dist/{types.tools-R0xGhiBa.d.cts → types.tools-DcFBsfRV.d.cts} +105 -1
- package/dist/{types.tools-R0xGhiBa.d.ts → types.tools-DcFBsfRV.d.ts} +105 -1
- package/package.json +1 -1
- package/schema/extension-manifest.schema.json +39 -0
- package/src/background.test.ts +33 -0
- package/src/background.ts +9 -0
- package/src/index.ts +5 -0
- package/src/messages.ts +3 -0
- package/src/runtime/accountsApi.ts +26 -0
- package/src/runtime/executionContext.test.ts +46 -4
- package/src/runtime/executionContext.ts +10 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime.ts +15 -3
- package/src/schemas/accounts.schema.test.ts +58 -0
- package/src/schemas/card.schema.test.ts +24 -0
- package/src/schemas/card.schema.ts +13 -0
- package/src/schemas/contributions.schema.ts +48 -0
- package/src/schemas/index.ts +6 -0
- package/src/schemas/permissions.schema.ts +2 -0
- package/src/types.components.ts +27 -0
- package/src/types.context.ts +68 -0
- package/src/types.contributions.ts +47 -0
- package/src/types.permissions.ts +8 -0
- package/src/types.ts +6 -0
package/src/runtime.ts
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
buildUserStorageAPI,
|
|
58
58
|
buildExtensionSecretsAPI,
|
|
59
59
|
buildUserSecretsAPI,
|
|
60
|
+
buildUserAccountsAPI,
|
|
60
61
|
createExecutionContext,
|
|
61
62
|
} from './runtime/index.js'
|
|
62
63
|
|
|
@@ -358,7 +359,7 @@ async function handleSchedulerFire(payload: SchedulerFirePayload): Promise<void>
|
|
|
358
359
|
sendRequest,
|
|
359
360
|
extensionContext!,
|
|
360
361
|
payload.userId,
|
|
361
|
-
grantedPermissions
|
|
362
|
+
grantedPermissions
|
|
362
363
|
)
|
|
363
364
|
|
|
364
365
|
// Run callbacks concurrently to avoid blocking
|
|
@@ -560,7 +561,7 @@ async function handleToolExecuteRequest(
|
|
|
560
561
|
sendRequest,
|
|
561
562
|
extensionContext!,
|
|
562
563
|
payload.userId,
|
|
563
|
-
grantedPermissions
|
|
564
|
+
grantedPermissions
|
|
564
565
|
)
|
|
565
566
|
|
|
566
567
|
const result = await tool.execute(payload.params, execContext)
|
|
@@ -608,7 +609,7 @@ async function handleActionExecuteRequest(
|
|
|
608
609
|
sendRequest,
|
|
609
610
|
extensionContext!,
|
|
610
611
|
payload.userId,
|
|
611
|
-
grantedPermissions
|
|
612
|
+
grantedPermissions
|
|
612
613
|
)
|
|
613
614
|
|
|
614
615
|
const result = await action.execute(payload.params, execContext)
|
|
@@ -990,6 +991,9 @@ function buildContext(
|
|
|
990
991
|
createUserStorageAPI: (userId) => buildUserStorageAPI(sendRequest, userId),
|
|
991
992
|
createSecretsAPI: () => buildExtensionSecretsAPI(sendRequest),
|
|
992
993
|
createUserSecretsAPI: (userId) => buildUserSecretsAPI(sendRequest, userId),
|
|
994
|
+
...(hasPermission('accounts.use')
|
|
995
|
+
? { createUserAccountsAPI: (userId: string) => buildUserAccountsAPI(sendRequest, userId) }
|
|
996
|
+
: {}),
|
|
993
997
|
})
|
|
994
998
|
}
|
|
995
999
|
|
|
@@ -1039,6 +1043,9 @@ export type {
|
|
|
1039
1043
|
ExecutionContext,
|
|
1040
1044
|
ExtensionContext,
|
|
1041
1045
|
ExtensionModule,
|
|
1046
|
+
ConnectedAccountsAPI,
|
|
1047
|
+
ConnectedAccount,
|
|
1048
|
+
AccountAccessToken,
|
|
1042
1049
|
Disposable,
|
|
1043
1050
|
AIProvider,
|
|
1044
1051
|
Tool,
|
|
@@ -1050,6 +1057,11 @@ export type {
|
|
|
1050
1057
|
ActionResult,
|
|
1051
1058
|
ModelInfo,
|
|
1052
1059
|
ChatMessage,
|
|
1060
|
+
// What a message can carry. A provider extension reads these on every user turn,
|
|
1061
|
+
// and until now had to reach for the root entry to name them — or declare them
|
|
1062
|
+
// itself, which is what two of them did.
|
|
1063
|
+
ChatImage,
|
|
1064
|
+
ChatFile,
|
|
1053
1065
|
ChatOptions,
|
|
1054
1066
|
GetModelsOptions,
|
|
1055
1067
|
ModelCapabilities,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { AccountContributionSchema, ExtensionContributionsSchema } from './contributions.schema.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What an extension may ask of a connected account.
|
|
6
|
+
*
|
|
7
|
+
* Every enabled extension's scopes go into one sign-in request. A scope Microsoft
|
|
8
|
+
* rejects therefore breaks the sign-in for all of them, which is why the schema
|
|
9
|
+
* is stricter than "any string".
|
|
10
|
+
*/
|
|
11
|
+
describe('AccountContributionSchema', () => {
|
|
12
|
+
it('accepts Graph permission names', () => {
|
|
13
|
+
const result = AccountContributionSchema.safeParse({
|
|
14
|
+
provider: 'microsoft',
|
|
15
|
+
scopes: ['Mail.Read', 'Mail.Send', 'Calendars.ReadWrite', 'Calendars.Read.Shared'],
|
|
16
|
+
reason: { en: 'Read your mail', sv: 'Läsa din mail' },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
expect(result.success).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Microsoft issues a token for one resource at a time. An Exchange scope next to
|
|
23
|
+
// Graph scopes makes the whole combined request fail.
|
|
24
|
+
it('refuses a scope for another resource', () => {
|
|
25
|
+
const result = AccountContributionSchema.safeParse({
|
|
26
|
+
provider: 'microsoft',
|
|
27
|
+
scopes: ['https://outlook.office.com/IMAP.AccessAsUser.All'],
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(result.success).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('refuses the scopes the host asks for itself', () => {
|
|
34
|
+
for (const scope of ['offline_access', 'User.Read', 'user.read', 'openid']) {
|
|
35
|
+
const result = AccountContributionSchema.safeParse({ provider: 'microsoft', scopes: [scope] })
|
|
36
|
+
expect(result.success, scope).toBe(false)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('needs at least one scope', () => {
|
|
41
|
+
expect(AccountContributionSchema.safeParse({ provider: 'microsoft', scopes: [] }).success).toBe(false)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('knows only the providers the host can sign in to', () => {
|
|
45
|
+
expect(
|
|
46
|
+
AccountContributionSchema.safeParse({ provider: 'google', scopes: ['Mail.Read'] }).success
|
|
47
|
+
).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('is part of what a manifest may contribute', () => {
|
|
51
|
+
const result = ExtensionContributionsSchema.safeParse({
|
|
52
|
+
accounts: [{ provider: 'microsoft', scopes: ['Calendars.ReadWrite'] }],
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(result.success).toBe(true)
|
|
56
|
+
expect(result.success && result.data.accounts?.[0]?.scopes).toEqual(['Calendars.ReadWrite'])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -30,6 +30,30 @@ describe('validateChatCard', () => {
|
|
|
30
30
|
expect(result.ok).toBe(true)
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
+
it('accepts a quotation with its attribution', () => {
|
|
34
|
+
const result = validateChatCard({
|
|
35
|
+
component: 'Quote',
|
|
36
|
+
text: 'Fyll gärna i del två i mallen innan vi ses.',
|
|
37
|
+
source: 'Maria Lind',
|
|
38
|
+
context: 'i morse 07:42',
|
|
39
|
+
footer: 'Ur mejlet om medarbetarsamtalet',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(result.ok).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('rejects markup properties on a quotation', () => {
|
|
46
|
+
// Quoted text is printed as it arrived. A property that implied formatting
|
|
47
|
+
// would let someone else's words be dressed up as Stina's own.
|
|
48
|
+
const result = validateChatCard({
|
|
49
|
+
component: 'Quote',
|
|
50
|
+
text: 'Fyll gärna i del två.',
|
|
51
|
+
variant: 'accent',
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
expect(result.ok).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
33
57
|
it('accepts an icon beside every heading that has one', () => {
|
|
34
58
|
for (const card of [
|
|
35
59
|
{ component: 'Chart', icon: 'temperature', series: [{ points: [1] }] },
|
|
@@ -333,6 +333,17 @@ const NoteCardSchema = z
|
|
|
333
333
|
})
|
|
334
334
|
.strict()
|
|
335
335
|
|
|
336
|
+
const QuoteCardSchema = z
|
|
337
|
+
.object({
|
|
338
|
+
component: z.literal('Quote'),
|
|
339
|
+
text: z.string(),
|
|
340
|
+
source: z.string().optional(),
|
|
341
|
+
context: z.string().optional(),
|
|
342
|
+
footer: z.string().optional(),
|
|
343
|
+
style,
|
|
344
|
+
})
|
|
345
|
+
.strict()
|
|
346
|
+
|
|
336
347
|
const CalendarEventCardSchema = z
|
|
337
348
|
.object({
|
|
338
349
|
component: z.literal('CalendarEvent'),
|
|
@@ -373,6 +384,7 @@ export const CHAT_CARD_COMPONENTS = [
|
|
|
373
384
|
'KeyValueList',
|
|
374
385
|
'Timeline',
|
|
375
386
|
'Note',
|
|
387
|
+
'Quote',
|
|
376
388
|
'CalendarEvent',
|
|
377
389
|
] as const
|
|
378
390
|
|
|
@@ -406,6 +418,7 @@ const CARD_MEMBERS = [
|
|
|
406
418
|
KeyValueListCardSchema,
|
|
407
419
|
TimelineCardSchema,
|
|
408
420
|
NoteCardSchema,
|
|
421
|
+
QuoteCardSchema,
|
|
409
422
|
CalendarEventCardSchema,
|
|
410
423
|
] as const
|
|
411
424
|
|
|
@@ -304,6 +304,48 @@ export const StorageContributionsSchema = z
|
|
|
304
304
|
})
|
|
305
305
|
.describe('Storage contributions')
|
|
306
306
|
|
|
307
|
+
// =============================================================================
|
|
308
|
+
// Connected Accounts
|
|
309
|
+
// =============================================================================
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* A Microsoft Graph delegated permission name: `Mail.Read`, `Calendars.ReadWrite`.
|
|
313
|
+
*
|
|
314
|
+
* Bare names only. A full URI would name some other resource, and Microsoft hands
|
|
315
|
+
* out tokens for one resource at a time - a manifest mixing Graph with, say,
|
|
316
|
+
* `https://outlook.office.com/IMAP.AccessAsUser.All` would make the combined
|
|
317
|
+
* sign-in fail for every extension, not just the one that asked.
|
|
318
|
+
*/
|
|
319
|
+
export const GRAPH_SCOPE_PATTERN = /^[A-Za-z]+(\.[A-Za-z]+)+$/
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Scopes the host always asks for and therefore does not accept from a manifest:
|
|
323
|
+
* a declared one would be redundant at best.
|
|
324
|
+
*/
|
|
325
|
+
export const HOST_ACCOUNT_SCOPES = ['offline_access', 'User.Read', 'openid', 'profile', 'email'] as const
|
|
326
|
+
|
|
327
|
+
export const AccountProviderSchema = z.enum(['microsoft']).describe('External account provider')
|
|
328
|
+
|
|
329
|
+
export const AccountContributionSchema = z
|
|
330
|
+
.object({
|
|
331
|
+
provider: AccountProviderSchema,
|
|
332
|
+
scopes: z
|
|
333
|
+
.array(
|
|
334
|
+
z
|
|
335
|
+
.string()
|
|
336
|
+
.regex(GRAPH_SCOPE_PATTERN, 'Must be a Microsoft Graph permission name, such as "Mail.Read"')
|
|
337
|
+
.refine(
|
|
338
|
+
(scope) =>
|
|
339
|
+
!HOST_ACCOUNT_SCOPES.some((hostScope) => hostScope.toLowerCase() === scope.toLowerCase()),
|
|
340
|
+
{ message: 'Requested by the host already; leave it out' }
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
.min(1)
|
|
344
|
+
.describe('Microsoft Graph delegated permissions'),
|
|
345
|
+
reason: LocalizedStringSchema.optional().describe('Why the extension needs them'),
|
|
346
|
+
})
|
|
347
|
+
.describe('An external account the extension works through')
|
|
348
|
+
|
|
307
349
|
// =============================================================================
|
|
308
350
|
// Extension Contributions
|
|
309
351
|
// =============================================================================
|
|
@@ -324,6 +366,10 @@ export const ExtensionContributionsSchema = z
|
|
|
324
366
|
commands: z.array(CommandDefinitionSchema).optional().describe('Slash commands'),
|
|
325
367
|
prompts: z.array(PromptContributionSchema).optional().describe('Prompt contributions'),
|
|
326
368
|
storage: StorageContributionsSchema.optional().describe('Storage collection declarations'),
|
|
369
|
+
accounts: z
|
|
370
|
+
.array(AccountContributionSchema)
|
|
371
|
+
.optional()
|
|
372
|
+
.describe("External accounts the extension works through, signed in once in Stina's settings"),
|
|
327
373
|
})
|
|
328
374
|
.describe('What an extension can contribute to Stina')
|
|
329
375
|
|
|
@@ -353,4 +399,6 @@ export type PromptSection = z.infer<typeof PromptSectionSchema>
|
|
|
353
399
|
export type PromptContribution = z.infer<typeof PromptContributionSchema>
|
|
354
400
|
export type StorageCollectionConfig = z.infer<typeof StorageCollectionConfigSchema>
|
|
355
401
|
export type StorageContributions = z.infer<typeof StorageContributionsSchema>
|
|
402
|
+
export type AccountProvider = z.infer<typeof AccountProviderSchema>
|
|
403
|
+
export type AccountContribution = z.infer<typeof AccountContributionSchema>
|
|
356
404
|
export type ExtensionContributions = z.infer<typeof ExtensionContributionsSchema>
|
package/src/schemas/index.ts
CHANGED
|
@@ -58,6 +58,12 @@ export {
|
|
|
58
58
|
CommandDefinitionSchema,
|
|
59
59
|
PromptContributionSchema,
|
|
60
60
|
PromptSectionSchema,
|
|
61
|
+
AccountProviderSchema,
|
|
62
|
+
AccountContributionSchema,
|
|
63
|
+
GRAPH_SCOPE_PATTERN,
|
|
64
|
+
HOST_ACCOUNT_SCOPES,
|
|
65
|
+
type AccountProvider,
|
|
66
|
+
type AccountContribution,
|
|
61
67
|
type ExtensionContributions,
|
|
62
68
|
type LocalizedString,
|
|
63
69
|
type ToolSettingsViewDefinition,
|
|
@@ -21,6 +21,7 @@ export const VALID_PERMISSIONS = [
|
|
|
21
21
|
'chat.history.read',
|
|
22
22
|
'chat.current.read',
|
|
23
23
|
'attachments.read',
|
|
24
|
+
'accounts.use',
|
|
24
25
|
'chat.message.write',
|
|
25
26
|
'provider.register',
|
|
26
27
|
'tools.register',
|
|
@@ -83,6 +84,7 @@ const UserDataPermissionSchema = z
|
|
|
83
84
|
'chat.history.read',
|
|
84
85
|
'chat.current.read',
|
|
85
86
|
'attachments.read',
|
|
87
|
+
'accounts.use',
|
|
86
88
|
])
|
|
87
89
|
.describe('User data access permission')
|
|
88
90
|
|
package/src/types.components.ts
CHANGED
|
@@ -886,6 +886,33 @@ export interface NoteProps extends ExtensionComponentData {
|
|
|
886
886
|
variant?: NoteVariant
|
|
887
887
|
}
|
|
888
888
|
|
|
889
|
+
/**
|
|
890
|
+
* The extension API properties for the Quote component.
|
|
891
|
+
*
|
|
892
|
+
* Someone else's words, set apart and attributed. For the sentence out of a mail
|
|
893
|
+
* that actually answers the question, the line from a document worth reading as
|
|
894
|
+
* written, the message a colleague sent.
|
|
895
|
+
*
|
|
896
|
+
* The text is rendered as written and never as markdown, which is the whole
|
|
897
|
+
* point of it existing beside `Note`: a link or a heading inside quoted text
|
|
898
|
+
* would be formatted as though Stina had put it there. `Note` is for the user's
|
|
899
|
+
* own writing and for hers; this is for everyone else's.
|
|
900
|
+
*
|
|
901
|
+
* Quote what bears quoting, not the whole message. A card is a thing to glance
|
|
902
|
+
* at, and a page of pasted mail is a page nobody reads.
|
|
903
|
+
*/
|
|
904
|
+
export interface QuoteProps extends ExtensionComponentData {
|
|
905
|
+
component: 'Quote'
|
|
906
|
+
/** The words themselves, verbatim. Rendered as written, never as markdown. */
|
|
907
|
+
text: string
|
|
908
|
+
/** Who wrote or said it, as they are known: a name, an address, a title. */
|
|
909
|
+
source?: string
|
|
910
|
+
/** When or where it came from: `i morse 07:42`, `i mötesanteckningarna`. */
|
|
911
|
+
context?: string
|
|
912
|
+
/** One quiet line under the quotation, for what is worth adding about it. */
|
|
913
|
+
footer?: string
|
|
914
|
+
}
|
|
915
|
+
|
|
889
916
|
/** Whether an event is going ahead. */
|
|
890
917
|
export type CalendarEventStatus = 'confirmed' | 'tentative' | 'cancelled'
|
|
891
918
|
|
package/src/types.context.ts
CHANGED
|
@@ -94,6 +94,74 @@ export interface ExecutionContext {
|
|
|
94
94
|
* so in its manifest and check before reaching for them.
|
|
95
95
|
*/
|
|
96
96
|
readonly attachments?: AttachmentsAPI
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The external accounts this user has connected to Stina.
|
|
100
|
+
*
|
|
101
|
+
* Present only for an extension holding `accounts.use`, and only on a request
|
|
102
|
+
* that knows whose it is - an account belongs to somebody, like an attachment.
|
|
103
|
+
*/
|
|
104
|
+
readonly accounts?: ConnectedAccountsAPI
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Working through an account the user signed in to in Stina's settings.
|
|
109
|
+
*
|
|
110
|
+
* The user connects a Microsoft account once, and every extension that declares
|
|
111
|
+
* Microsoft under `contributes.accounts` can use it. The extension never sees the
|
|
112
|
+
* refresh token; it asks for an access token when it needs one and gets a fresh
|
|
113
|
+
* one back, refreshed by the host when the old one has run out.
|
|
114
|
+
*
|
|
115
|
+
* One thing the host cannot narrow: Microsoft issues a Graph token carrying every
|
|
116
|
+
* permission the user has granted Stina, not only the ones this extension
|
|
117
|
+
* declared. The declaration decides whether an extension gets a token at all,
|
|
118
|
+
* and what the user is asked to approve - not what the token can do.
|
|
119
|
+
*/
|
|
120
|
+
export interface ConnectedAccountsAPI {
|
|
121
|
+
/**
|
|
122
|
+
* The accounts this user has connected for a provider, newest first.
|
|
123
|
+
*
|
|
124
|
+
* An empty list is the normal answer for a user who has not connected one yet:
|
|
125
|
+
* the extension should point them at Stina's settings rather than start a
|
|
126
|
+
* sign-in of its own.
|
|
127
|
+
*/
|
|
128
|
+
list(provider: 'microsoft'): Promise<ConnectedAccount[]>
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* An access token for one of those accounts, for the scopes this extension
|
|
132
|
+
* declared.
|
|
133
|
+
*
|
|
134
|
+
* Rejects when the account is gone, when the user has not granted every
|
|
135
|
+
* declared scope yet, or when the provider has revoked the sign-in. The
|
|
136
|
+
* message says which, in words the user can act on - it is fine to show it.
|
|
137
|
+
*/
|
|
138
|
+
getAccessToken(accountId: string): Promise<AccountAccessToken>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** An account the user has connected, as an extension sees it. */
|
|
142
|
+
export interface ConnectedAccount {
|
|
143
|
+
/** Stable id, to store alongside whatever the extension keeps for this account. */
|
|
144
|
+
id: string
|
|
145
|
+
provider: 'microsoft'
|
|
146
|
+
/** The address the user signs in with. */
|
|
147
|
+
email: string
|
|
148
|
+
displayName?: string
|
|
149
|
+
/**
|
|
150
|
+
* Whether the user has granted every scope this extension declared. False when
|
|
151
|
+
* the extension was installed after the account was connected, and the user has
|
|
152
|
+
* yet to reconnect it in the settings.
|
|
153
|
+
*/
|
|
154
|
+
hasRequiredScopes: boolean
|
|
155
|
+
/** Set when the provider has refused the stored sign-in and it has to be redone. */
|
|
156
|
+
needsReconnect: boolean
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** An access token, and when it stops working. */
|
|
160
|
+
export interface AccountAccessToken {
|
|
161
|
+
/** Bearer token for Microsoft Graph. */
|
|
162
|
+
token: string
|
|
163
|
+
/** ISO timestamp. Ask again after this; the host refreshes. */
|
|
164
|
+
expiresAt: string
|
|
97
165
|
}
|
|
98
166
|
|
|
99
167
|
/**
|
|
@@ -25,6 +25,8 @@ export interface ExtensionContributions {
|
|
|
25
25
|
commands?: CommandDefinition[]
|
|
26
26
|
/** Prompt contributions for the system prompt */
|
|
27
27
|
prompts?: PromptContribution[]
|
|
28
|
+
/** External accounts the extension works through, signed in once in Stina's settings */
|
|
29
|
+
accounts?: AccountContribution[]
|
|
28
30
|
/** Storage collection declarations */
|
|
29
31
|
storage?: {
|
|
30
32
|
collections: {
|
|
@@ -36,6 +38,51 @@ export interface ExtensionContributions {
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Connected Accounts
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An external account provider the host can sign in to on the user's behalf.
|
|
47
|
+
*
|
|
48
|
+
* Only Microsoft for now, and only through Microsoft Graph: mail and calendar
|
|
49
|
+
* then share one sign-in and one token, instead of an IMAP token and a Graph
|
|
50
|
+
* token that an admin has to approve separately.
|
|
51
|
+
*/
|
|
52
|
+
export type AccountProvider = 'microsoft'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What an extension needs from an account the user connects to Stina.
|
|
56
|
+
*
|
|
57
|
+
* Stina asks the provider for every scope that the enabled extensions declare,
|
|
58
|
+
* all at once, so that the user - and, at work, their administrator - approves
|
|
59
|
+
* one request instead of one per extension. Declaring a scope here is therefore
|
|
60
|
+
* also what makes it show up in that request, and in the list of what is asked
|
|
61
|
+
* for in the settings.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```json
|
|
65
|
+
* "accounts": [
|
|
66
|
+
* {
|
|
67
|
+
* "provider": "microsoft",
|
|
68
|
+
* "scopes": ["Calendars.ReadWrite"],
|
|
69
|
+
* "reason": { "en": "Read and book events in your calendar", "sv": "Läsa och boka i din kalender" }
|
|
70
|
+
* }
|
|
71
|
+
* ]
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export interface AccountContribution {
|
|
75
|
+
provider: AccountProvider
|
|
76
|
+
/**
|
|
77
|
+
* Microsoft Graph delegated permissions, by name: `Mail.Read`,
|
|
78
|
+
* `Calendars.ReadWrite`. The host adds `offline_access` and `User.Read`
|
|
79
|
+
* itself, so they are not declared here.
|
|
80
|
+
*/
|
|
81
|
+
scopes: string[]
|
|
82
|
+
/** Why the extension needs them, shown next to the scopes in the settings. */
|
|
83
|
+
reason?: LocalizedString
|
|
84
|
+
}
|
|
85
|
+
|
|
39
86
|
// ============================================================================
|
|
40
87
|
// Tool Settings Views
|
|
41
88
|
// ============================================================================
|
package/src/types.permissions.ts
CHANGED
|
@@ -35,6 +35,14 @@ export type UserDataPermission =
|
|
|
35
35
|
* extension.
|
|
36
36
|
*/
|
|
37
37
|
| 'attachments.read'
|
|
38
|
+
/**
|
|
39
|
+
* Get access tokens for the external accounts the user has connected to Stina
|
|
40
|
+
* itself - a Microsoft account, say - for the scopes the extension declares under
|
|
41
|
+
* `contributes.accounts`. The user signs in once, in Stina's settings, and every
|
|
42
|
+
* extension that declares the provider can use that sign-in. Refresh tokens stay
|
|
43
|
+
* with the host.
|
|
44
|
+
*/
|
|
45
|
+
| 'accounts.use'
|
|
38
46
|
|
|
39
47
|
/** Capability permissions */
|
|
40
48
|
export type CapabilityPermission =
|
package/src/types.ts
CHANGED
|
@@ -47,6 +47,9 @@ export type {
|
|
|
47
47
|
// Prompts
|
|
48
48
|
PromptSection,
|
|
49
49
|
PromptContribution,
|
|
50
|
+
// Connected accounts
|
|
51
|
+
AccountProvider,
|
|
52
|
+
AccountContribution,
|
|
50
53
|
} from './types.contributions.js'
|
|
51
54
|
|
|
52
55
|
// Manifest
|
|
@@ -103,6 +106,9 @@ export type {
|
|
|
103
106
|
BackgroundWorkersAPI,
|
|
104
107
|
AttachmentsAPI,
|
|
105
108
|
AttachmentContent,
|
|
109
|
+
ConnectedAccountsAPI,
|
|
110
|
+
ConnectedAccount,
|
|
111
|
+
AccountAccessToken,
|
|
106
112
|
} from './types.context.js'
|
|
107
113
|
|
|
108
114
|
// Storage and Secrets
|