@lanes-sh/link 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -8
- package/instructions/skills/lanes-link/SKILL.md +42 -14
- package/package.json +1 -1
- package/src/cli/argv.ts +50 -0
- package/src/cli/brand.ts +178 -0
- package/src/cli/callback-page.ts +108 -128
- package/src/cli/commands/connect/accounts.ts +5 -0
- package/src/cli/commands/connect/assertion.ts +187 -0
- package/src/cli/commands/connect/authorise.ts +61 -17
- package/src/cli/commands/connect/client.ts +37 -9
- package/src/cli/commands/connect/discover.ts +94 -0
- package/src/cli/commands/connect/family.ts +72 -0
- package/src/cli/commands/connect/index.ts +113 -115
- package/src/cli/commands/connect/method.ts +237 -0
- package/src/cli/commands/connect/outcome.ts +42 -1
- package/src/cli/commands/connect/pasted-token.ts +66 -0
- package/src/cli/commands/connect/requirements.ts +60 -8
- package/src/cli/commands/connect/setup.ts +16 -5
- package/src/cli/commands/connect/target-note.ts +34 -0
- package/src/cli/commands/identity.ts +258 -0
- package/src/cli/commands/knowledge/index.ts +390 -0
- package/src/cli/commands/knowledge/migrate.ts +180 -0
- package/src/cli/commands/knowledge/setup.ts +144 -0
- package/src/cli/commands/knowledge.ts +10 -0
- package/src/cli/commands/mcp/harnesses.ts +16 -2
- package/src/cli/commands/mcp/register.ts +9 -1
- package/src/cli/commands/mcp/stdio.ts +21 -0
- package/src/cli/commands/operate/dashboard.ts +107 -0
- package/src/cli/commands/operate/findings.ts +151 -0
- package/src/cli/commands/operate/inspect.ts +63 -164
- package/src/cli/commands/operate/outputs.ts +38 -11
- package/src/cli/commands/operate/policy.ts +7 -7
- package/src/cli/commands/operate/serve.ts +3 -0
- package/src/cli/commands/operate/status.ts +108 -1
- package/src/cli/commands/operate/token.ts +1 -1
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/profile/declare.ts +154 -0
- package/src/cli/commands/profile/removal.ts +17 -0
- package/src/cli/commands/profile/remove.ts +5 -5
- package/src/cli/commands/profile.ts +83 -35
- package/src/cli/commands/secrets.ts +6 -6
- package/src/cli/commands/setup.ts +22 -6
- package/src/cli/commands/sync.ts +262 -0
- package/src/cli/commands/target.ts +65 -83
- package/src/cli/config-edit.ts +53 -144
- package/src/cli/config-repair.ts +186 -0
- package/src/cli/dashboard-page.ts +284 -0
- package/src/cli/dashboard-shell.ts +125 -0
- package/src/cli/dispatch-owner.ts +93 -0
- package/src/cli/identity.ts +12 -1
- package/src/cli/main.ts +90 -61
- package/src/cli/nearest.ts +45 -0
- package/src/cli/oauth-callback.ts +187 -0
- package/src/cli/oauth-exchange.ts +57 -15
- package/src/cli/oauth.ts +67 -177
- package/src/cli/output.ts +21 -5
- package/src/cli/provider-marks.ts +45 -0
- package/src/cli/runtime/open.ts +74 -51
- package/src/cli/runtime/registry.ts +60 -2
- package/src/cli/runtime/select.ts +26 -13
- package/src/cli/runtime/vault.ts +61 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection.ts +365 -0
- package/src/cli/usage.ts +42 -11
- package/src/connectivity/auth/README.md +7 -1
- package/src/connectivity/auth/basic/index.ts +1 -1
- package/src/connectivity/auth/index.ts +14 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
- package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
- package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
- package/src/connectivity/auth/oauth-jwt/README.md +33 -0
- package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
- package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
- package/src/connectivity/auth/resolve.ts +1 -1
- package/src/connectivity/auth/token.ts +11 -0
- package/src/connectivity/index.ts +2 -0
- package/src/connectivity/manifest/auth.ts +99 -2
- package/src/connectivity/manifest/identity.ts +12 -0
- package/src/connectivity/manifest/index.ts +3 -1
- package/src/connectivity/manifest/provider.ts +37 -8
- package/src/connectivity/manifest/requirements.ts +109 -6
- package/src/deployments/adapters/filesystem.ts +10 -1
- package/src/deployments/adapters/github-api.ts +106 -0
- package/src/deployments/adapters/github-commit.ts +103 -0
- package/src/deployments/adapters/github-repo.ts +356 -0
- package/src/deployments/adapters/github-testing.ts +258 -0
- package/src/deployments/adapters/github.ts +125 -0
- package/src/deployments/deploy.ts +94 -114
- package/src/deployments/discover.ts +103 -0
- package/src/deployments/driver.ts +8 -1
- package/src/deployments/gcp/driver.ts +3 -1
- package/src/deployments/knowledge.ts +119 -0
- package/src/deployments/prepare.ts +12 -6
- package/src/deployments/report.ts +117 -0
- package/src/deployments/servable.ts +82 -0
- package/src/deployments/serving.ts +165 -0
- package/src/deployments/sync-apply.ts +276 -0
- package/src/deployments/sync.ts +136 -0
- package/src/deployments/target.ts +3 -2
- package/src/deployments/upload.ts +19 -12
- package/src/dispatch/dispatch.ts +1 -1
- package/src/profile/deployments.ts +80 -0
- package/src/profile/identity.ts +60 -0
- package/src/profile/index.ts +23 -5
- package/src/profile/knowledge.ts +124 -0
- package/src/profile/load.ts +17 -5
- package/src/profile/primitives.ts +24 -1
- package/src/profile/schema.ts +81 -3
- package/src/profile/targets.ts +122 -109
- package/src/profile/workspace.ts +139 -79
- package/src/providers/google/calendar/index.ts +2 -0
- package/src/providers/google/contacts/index.ts +2 -0
- package/src/providers/google/docs/index.ts +2 -0
- package/src/providers/google/drive/index.ts +2 -0
- package/src/providers/google/gmail/index.ts +2 -0
- package/src/providers/google/gmail-imap/index.ts +125 -0
- package/src/providers/google/index.ts +2 -1
- package/src/providers/google/shared/oauth.ts +18 -6
- package/src/providers/google/shared/service-account.ts +110 -0
- package/src/providers/google/shared/setup.ts +21 -3
- package/src/providers/google/sheets/index.ts +2 -0
- package/src/providers/google/tasks/index.ts +2 -0
- package/src/providers/identity/provider.ts +166 -0
- package/src/providers/index.ts +3 -0
- package/src/providers/owner.ts +10 -2
- package/src/providers/scopes.ts +2 -0
- package/src/providers/setup/plan.ts +31 -9
- package/src/providers/setup/provider.ts +23 -0
- package/src/providers/slack/index.ts +81 -33
- package/src/providers/slack/oauth.ts +103 -0
- package/src/providers/slack/scopes.ts +37 -0
- package/src/server/container.ts +18 -1
- package/src/server/cors.ts +252 -0
- package/src/server/dashboard.ts +208 -0
- package/src/server/endpoint.ts +45 -1
- package/src/server/generations.ts +11 -2
- package/src/server/harness.ts +7 -0
- package/src/server/index.ts +45 -6
- package/src/server/mcp/index.ts +1 -0
- package/src/server/mcp/instructions.ts +28 -1
- package/src/server/mcp/visibility.ts +33 -0
- package/src/stores/blobs/route.ts +123 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gmail over IMAP, for the account that cannot use either other route.
|
|
5
|
+
*
|
|
6
|
+
* A personal Google account has exactly one way to a mailbox that does not
|
|
7
|
+
* expire, and this is it. The two alternatives both fail for a reason that is
|
|
8
|
+
* not about effort:
|
|
9
|
+
*
|
|
10
|
+
* - The REST provider (`gmail`) authorises in a browser, and an OAuth client
|
|
11
|
+
* left in "Testing" has every refresh token it issues expired after seven
|
|
12
|
+
* days. Publishing the client fixes that and is the better answer where it is
|
|
13
|
+
* available — see ADR-038 and `docs/detailed/setup/google.md`.
|
|
14
|
+
* - The key route (`auth.assertion` on `gmail`) does not apply at all. A service
|
|
15
|
+
* account has no mailbox of its own, so it can only reach one by acting as
|
|
16
|
+
* somebody, and that grant is domain-wide delegation — made in a Workspace
|
|
17
|
+
* admin console that a personal account does not have.
|
|
18
|
+
*
|
|
19
|
+
* An app password has neither problem. It is issued by the account holder to
|
|
20
|
+
* themselves, it does not expire, and IMAP asks no authorization server for
|
|
21
|
+
* anything. What it costs is reach and shape: this is a mailbox over IMAP and
|
|
22
|
+
* SMTP, so it is the mail capability set rather than Gmail's API — no labels
|
|
23
|
+
* vocabulary, no threads resource, no drafts. Searching, reading, flagging,
|
|
24
|
+
* moving and sending, which is most of what a mailbox is for.
|
|
25
|
+
*
|
|
26
|
+
* The mirror image of `../shared/service-account.ts`, and deliberately so: that
|
|
27
|
+
* one covers Workspace and not personal accounts, this one covers personal
|
|
28
|
+
* accounts and not Workspace. Google turned off basic authentication for
|
|
29
|
+
* Workspace in March 2025, and an administrator can disable app passwords
|
|
30
|
+
* outright, so an account under a domain should take one of the other two.
|
|
31
|
+
*
|
|
32
|
+
* A separate manifest rather than a route on `gmail` because a manifest has one
|
|
33
|
+
* connector, and IMAP is a different protocol from HTTPS — the same reason
|
|
34
|
+
* iCloud is three providers rather than one. It also makes the policy line
|
|
35
|
+
* separable: `gmail_imap.*` can be allowed without granting the REST surface,
|
|
36
|
+
* or the other way round.
|
|
37
|
+
*/
|
|
38
|
+
export const gmailImap = defineProvider({
|
|
39
|
+
id: 'gmail_imap',
|
|
40
|
+
name: 'Gmail (IMAP)',
|
|
41
|
+
description:
|
|
42
|
+
'Read, search, and send mail in a personal Gmail mailbox over IMAP and SMTP, with an app password that does not expire.',
|
|
43
|
+
connector: {
|
|
44
|
+
kind: 'imap',
|
|
45
|
+
host: 'imap.gmail.com',
|
|
46
|
+
port: 993,
|
|
47
|
+
smtp: {
|
|
48
|
+
host: 'smtp.gmail.com',
|
|
49
|
+
port: 587,
|
|
50
|
+
starttls: true,
|
|
51
|
+
// Gmail's limit is 25 MB for the whole encoded message rather than the
|
|
52
|
+
// 20 MB default, and the send path derives the usable weight of the files
|
|
53
|
+
// from it. Declaring the real number is what makes an oversized message
|
|
54
|
+
// refused before dialling rather than part-way through DATA.
|
|
55
|
+
max_message_bytes: 25 * 1024 * 1024,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
// Not a choice. `defineProvider` refuses anything else on an imap connector,
|
|
59
|
+
// because every mail host that matters issues an app password and expects it
|
|
60
|
+
// over Basic — and Gmail's OAuth path belongs to the REST provider.
|
|
61
|
+
auth: { kind: 'basic' },
|
|
62
|
+
// No `app`: this is one provider rather than a family, so the credential
|
|
63
|
+
// lands at `gmail_imap/<connection>` and is shared with nothing. An app
|
|
64
|
+
// password is issued per app rather than per account, so there would be
|
|
65
|
+
// nothing to share it with even if there were siblings.
|
|
66
|
+
identity: { kind: 'connector' },
|
|
67
|
+
setup: {
|
|
68
|
+
summary:
|
|
69
|
+
'Gmail over IMAP uses an app password — a sixteen-character password you issue to yourself, ' +
|
|
70
|
+
'which is not your Google account password and does not expire. Nothing is authorised in a ' +
|
|
71
|
+
'browser and nothing has to be re-approved later. This is the personal-account route: ' +
|
|
72
|
+
'Google turned off basic authentication for Workspace accounts in March 2025, and a Workspace ' +
|
|
73
|
+
'administrator can disable app passwords for the whole domain.',
|
|
74
|
+
docs: 'docs/detailed/setup/google.md',
|
|
75
|
+
docs_url: 'https://support.google.com/accounts/answer/185833',
|
|
76
|
+
steps: [
|
|
77
|
+
'Two-Step Verification has to be on. Without it Google does not offer app passwords at all, and the page below returns "the setting you are looking for is not available for your account" rather than saying why. Turn it on at https://myaccount.google.com/signinoptions/twosv.',
|
|
78
|
+
'Open https://myaccount.google.com/apppasswords and create one. Name it "Lanes Link" — the name is the only way to revoke this one later without cutting off your other devices.',
|
|
79
|
+
'Copy the sixteen characters. Google shows them once, in four groups of four; the spaces are cosmetic and it is accepted either way.',
|
|
80
|
+
'You are asked for your full address next, which is the one you sign in with, ending @gmail.com.',
|
|
81
|
+
'IMAP is on by default. If a login is refused with a mailbox error rather than an authentication error, check Gmail → Settings → See all settings → Forwarding and POP/IMAP → IMAP access.',
|
|
82
|
+
],
|
|
83
|
+
// What a transport cannot say for itself, because it must not know which
|
|
84
|
+
// vendor it is talking to. The first sentence is the mistake this route
|
|
85
|
+
// actually produces: an app password looks enough like a password that the
|
|
86
|
+
// account password gets pasted instead, and IMAP reports both identically.
|
|
87
|
+
troubleshooting:
|
|
88
|
+
'For Gmail this is almost always a Google account password used where an app password belongs, ' +
|
|
89
|
+
'or an account without Two-Step Verification — app passwords do not exist without it. Generate ' +
|
|
90
|
+
'one at https://myaccount.google.com/apppasswords and re-run: lanes link connect gmail_imap --replace.',
|
|
91
|
+
prompts: [
|
|
92
|
+
{
|
|
93
|
+
key: 'username',
|
|
94
|
+
label: 'Google account (the full email address)',
|
|
95
|
+
secret: false,
|
|
96
|
+
scope: 'connection' as const,
|
|
97
|
+
field: 'username' as const,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: 'password',
|
|
101
|
+
label: 'App password (sixteen characters)',
|
|
102
|
+
secret: true,
|
|
103
|
+
scope: 'connection' as const,
|
|
104
|
+
field: 'password' as const,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
// The mail capability set, so the same reasoning as `icloud_mail` applies
|
|
109
|
+
// verbatim: opted back in one key at a time, everything unlisted withheld.
|
|
110
|
+
redact: {
|
|
111
|
+
// Never the search terms — a query is content, and "who did I email about
|
|
112
|
+
// the diagnosis" is the whole message.
|
|
113
|
+
search_messages: ['mailbox', 'limit', 'unseen', 'flagged'],
|
|
114
|
+
get_message: ['mailbox', 'uid', 'include_body'],
|
|
115
|
+
mark_messages: ['mailbox', 'add_flags', 'remove_flags'],
|
|
116
|
+
// `destination_flag` alongside `destination`: two spellings of the same
|
|
117
|
+
// fact, and keeping only one means a junk move logs with no destination.
|
|
118
|
+
move_messages: ['mailbox', 'destination', 'destination_flag'],
|
|
119
|
+
// Nothing. The recipients and the body are the message, and `attachments`
|
|
120
|
+
// may literally contain a file. What was attached is recorded by the send
|
|
121
|
+
// path itself through `audit.annotate` — filename, size, type, SHA-256 and
|
|
122
|
+
// origin. Identifiers, not content.
|
|
123
|
+
send_message: [],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
/** Google's
|
|
1
|
+
/** Google's ten providers. Each folder holds all of its own vendor knowledge. */
|
|
2
2
|
export { calendar } from './calendar/index.ts';
|
|
3
3
|
export { contacts } from './contacts/index.ts';
|
|
4
4
|
export { docs } from './docs/index.ts';
|
|
5
5
|
export { drive, DRIVE_SCOPES } from './drive/index.ts';
|
|
6
6
|
export { driveMcp } from './drive-mcp/index.ts';
|
|
7
7
|
export { gmail, GMAIL_SCOPES } from './gmail/index.ts';
|
|
8
|
+
export { gmailImap } from './gmail-imap/index.ts';
|
|
8
9
|
export { gmailMcp } from './gmail-mcp/index.ts';
|
|
9
10
|
export { sheets } from './sheets/index.ts';
|
|
10
11
|
export { tasks } from './tasks/index.ts';
|
|
@@ -24,10 +24,18 @@ export const GOOGLE_APP = 'google';
|
|
|
24
24
|
* and there is no way to withdraw one copy of a secret.
|
|
25
25
|
*
|
|
26
26
|
* What this buys the operator is the whole of `setup/google.md`: no project, no
|
|
27
|
-
* console, no scope list to transcribe
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* console, and no scope list to transcribe. What it costs is recorded in
|
|
28
|
+
* ADR-028 and in the guarantee table in `docs/detailed/security.md` — chiefly
|
|
29
|
+
* that the exchange stops being local.
|
|
30
|
+
*
|
|
31
|
+
* What it does *not* buy, and used to claim to: escape from the seven-day
|
|
32
|
+
* refresh-token expiry. That expiry is a property of the client's publishing
|
|
33
|
+
* status rather than of its verification, and a client under review has the
|
|
34
|
+
* status it has — so a connection made this way is re-authorised weekly until
|
|
35
|
+
* the review lands, exactly like one made against a client of the operator's
|
|
36
|
+
* own that was left in "Testing". The way around it is not a different client.
|
|
37
|
+
* It is `auth.assertion`: a key does not expire because nothing consented, and
|
|
38
|
+
* `./service-account.ts` is that route.
|
|
31
39
|
*/
|
|
32
40
|
const BROKER_ORIGIN = 'https://api.lanes.sh';
|
|
33
41
|
const BROKER_PATH = '/v1/auth/link/google';
|
|
@@ -62,8 +70,12 @@ export const GOOGLE_OAUTH = {
|
|
|
62
70
|
authorize_params: { access_type: 'offline', prompt: 'select_account consent' },
|
|
63
71
|
// Every REST provider here spreads this block, so one line turns brokering on
|
|
64
72
|
// for all seven. `gmail_mcp` and `drive_mcp` write their auth longhand and do
|
|
65
|
-
// not spread it, which is what keeps them bring-your-own
|
|
66
|
-
//
|
|
73
|
+
// not spread it, which is what keeps them bring-your-own.
|
|
74
|
+
//
|
|
75
|
+
// That is now a choice rather than a constraint. ADR-040 made a broker legal
|
|
76
|
+
// on an mcp connector that names its own endpoints, so these two could follow
|
|
77
|
+
// Slack — what stops them is that nobody has established which client Google
|
|
78
|
+
// would have us use for its MCP servers, not that the machinery refuses.
|
|
67
79
|
broker: GOOGLE_BROKER,
|
|
68
80
|
} as const;
|
|
69
81
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { GOOGLE_APP } from './oauth.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The other way into a Google account: a key, instead of a browser.
|
|
5
|
+
*
|
|
6
|
+
* Google issues service accounts, and a service account holds a private key
|
|
7
|
+
* that does not expire. It is the answer to the one complaint the browser flow
|
|
8
|
+
* cannot fix — that a client left in "Testing" has its refresh tokens expired
|
|
9
|
+
* after seven days, so every connection has to be re-approved weekly until
|
|
10
|
+
* verification lands. A key is not subject to that, or to any other policy the
|
|
11
|
+
* issuer applies to consent, because nobody consented.
|
|
12
|
+
*
|
|
13
|
+
* What it costs is reach, and the cost is different per product, which is why
|
|
14
|
+
* `delegation` is a parameter rather than a constant. A service account is an
|
|
15
|
+
* identity in its own right: it has a Drive, and a calendar, and no mailbox and
|
|
16
|
+
* no contacts. So Drive, Sheets, Docs and Calendar work by *sharing* something
|
|
17
|
+
* with its address, and Gmail, Contacts and Tasks work only if a Workspace
|
|
18
|
+
* administrator lets the key act as a person. A personal Google account has no
|
|
19
|
+
* administrator, so for those three there is no key route at all and the
|
|
20
|
+
* walkthrough says so rather than letting someone find out later.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Where the key lives. One file per profile, covering every Google provider. */
|
|
24
|
+
export const GOOGLE_KEY_REF = `${GOOGLE_APP}/service_account_key`;
|
|
25
|
+
|
|
26
|
+
const SHARE_HINT: Record<string, string> = {
|
|
27
|
+
Drive: 'a folder (Share → paste the address → Editor)',
|
|
28
|
+
Sheets: 'a spreadsheet (Share → paste the address → Editor)',
|
|
29
|
+
Docs: 'a document (Share → paste the address → Editor)',
|
|
30
|
+
Calendar:
|
|
31
|
+
'a calendar (Settings for that calendar → "Share with specific people" → paste the address)',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where a personal account should go instead, for the products that have
|
|
36
|
+
* somewhere.
|
|
37
|
+
*
|
|
38
|
+
* Only Gmail does. A key needs delegation for mail, contacts and task lists
|
|
39
|
+
* alike, and delegation needs an administrator — but mail is the one Google
|
|
40
|
+
* still serves over a protocol that takes a password, so `gmail_imap` is a real
|
|
41
|
+
* answer rather than a consolation. Contacts and Tasks have none, and saying so
|
|
42
|
+
* plainly beats sending somebody to look.
|
|
43
|
+
*/
|
|
44
|
+
export const googleServiceAccount = (
|
|
45
|
+
product: string,
|
|
46
|
+
scopes: readonly string[],
|
|
47
|
+
delegation: 'optional' | 'required',
|
|
48
|
+
apis: readonly string[],
|
|
49
|
+
instead?: string,
|
|
50
|
+
) => ({
|
|
51
|
+
method: 'service_account',
|
|
52
|
+
label: 'Service account key',
|
|
53
|
+
delegation,
|
|
54
|
+
key_ref: GOOGLE_KEY_REF,
|
|
55
|
+
reach:
|
|
56
|
+
delegation === 'optional'
|
|
57
|
+
? `a JSON key that never expires, and no browser. Reaches only what you share with the ` +
|
|
58
|
+
`key's own address, so nothing in the account moves until you share it.`
|
|
59
|
+
: `a JSON key that never expires, and no browser. ${product} has nothing that belongs to ` +
|
|
60
|
+
`a key, so this needs a Google Workspace administrator to let it act as you — a personal ` +
|
|
61
|
+
`Google account cannot do it` +
|
|
62
|
+
(instead ? `, and wants ${instead} instead.` : '.'),
|
|
63
|
+
subject_label:
|
|
64
|
+
delegation === 'optional'
|
|
65
|
+
? 'Google account to act as, if an administrator has granted it'
|
|
66
|
+
: 'Google account to act as',
|
|
67
|
+
setup: {
|
|
68
|
+
summary:
|
|
69
|
+
delegation === 'optional'
|
|
70
|
+
? `${product} can authenticate with a service account key instead of signing in. The key ` +
|
|
71
|
+
`does not expire, so this is connected once and stays connected. It reaches only what ` +
|
|
72
|
+
`is shared with it, which is the trade: you pick what it can see, one resource at a time.`
|
|
73
|
+
: `${product} can authenticate with a service account key instead of signing in. The key ` +
|
|
74
|
+
`does not expire — but a key has no mailbox, contacts or task lists of its own, so this ` +
|
|
75
|
+
`route works only where a Google Workspace administrator has authorised the key to act ` +
|
|
76
|
+
`as a user in their domain. On a personal Google account, use the browser instead` +
|
|
77
|
+
(instead ? `, or ${instead}, which is an app password over IMAP and does not expire either.` : '.'),
|
|
78
|
+
docs: 'docs/detailed/setup/google.md',
|
|
79
|
+
docs_url: 'https://console.cloud.google.com/iam-admin/serviceaccounts',
|
|
80
|
+
steps: [
|
|
81
|
+
'Create or pick a project at https://console.cloud.google.com',
|
|
82
|
+
`Enable the APIs:\n gcloud services enable ${apis.join(' ')} --project=YOUR_PROJECT\n Without gcloud: APIs & Services → Library, and search for each by name.`,
|
|
83
|
+
'IAM & Admin → Service Accounts → Create service account. Name it "Lanes Link". Grant it no project roles — the roles page governs Google Cloud resources, and nothing here is one.',
|
|
84
|
+
'Open the account → Keys → Add key → Create new key → JSON. It downloads once. You are asked for the path to that file next; its contents go to the credential store and the file itself is not read again.',
|
|
85
|
+
...(delegation === 'required'
|
|
86
|
+
? [
|
|
87
|
+
'THIS PRODUCT NEEDS DOMAIN-WIDE DELEGATION, which only a Google Workspace administrator can grant. Copy the service account\'s numeric "Unique ID" (the client ID, not the email) from its Details tab.',
|
|
88
|
+
`In the Workspace Admin console → Security → Access and data control → API controls → Domain-wide delegation → Add new. Paste that client ID, and paste this scope list exactly:\n ${scopes.join(',\n ')}\n All of them, comma-separated, in one field. A partial list is refused the same way a missing one is, and the refusal does not say which scope was short.`,
|
|
89
|
+
'Delegation can take a few minutes to take effect. If the first connect is refused with "unauthorized_client", wait and run it again — nothing was stored.',
|
|
90
|
+
]
|
|
91
|
+
: [
|
|
92
|
+
`Share what you want reachable with the service account's email address — it ends in .iam.gserviceaccount.com and is printed once the key is stored. For ${product} that means ${SHARE_HINT[product] ?? 'the resource itself'}.`,
|
|
93
|
+
'Nothing else in the account is reachable, including files the same person owns. That is the point of this route, and it is also the thing to remember when something is "missing" — it has not been shared yet.',
|
|
94
|
+
'A Workspace administrator can instead grant domain-wide delegation, which lets the key act as a user and reach everything they can. The connect prompt asks which account to act as; leaving it blank means the key acts as itself.',
|
|
95
|
+
]),
|
|
96
|
+
],
|
|
97
|
+
prompts: [
|
|
98
|
+
{
|
|
99
|
+
key: 'service_account_key',
|
|
100
|
+
label: 'Path to the downloaded JSON key (or paste its contents)',
|
|
101
|
+
// Read from a path in the ordinary case, so this is not a secret typed
|
|
102
|
+
// into a terminal — but it may be pasted, and a pasted private key must
|
|
103
|
+
// not land in scrollback.
|
|
104
|
+
secret: true,
|
|
105
|
+
scope: 'shared' as const,
|
|
106
|
+
credential_ref: GOOGLE_KEY_REF,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
@@ -40,6 +40,21 @@ export const googleSetup = (
|
|
|
40
40
|
summary: `${product} signs in with Google. By default it authorises against the OAuth client Lanes operates, so there is nothing to register and no client secret on this machine — the code is exchanged for a token by the Lanes API, which holds that secret. Pass --own-client to register a client of your own instead; the steps below are that path, asked once per profile and then covering every Google account you connect.`,
|
|
41
41
|
docs: 'docs/detailed/setup/google.md',
|
|
42
42
|
docs_url: 'https://console.cloud.google.com/auth',
|
|
43
|
+
// AUDIENCE leads with Internal, and the ordering is the point rather than a
|
|
44
|
+
// preference. The seven-day `invalid_grant` everyone hits is a property of
|
|
45
|
+
// publishing status, and an Internal app has no publishing status to have —
|
|
46
|
+
// so it also has no expiry, no verification question, no unverified-app
|
|
47
|
+
// warning, no test-user list, and no scope registration. Steering a
|
|
48
|
+
// Workspace user to External costs them every one of those, to buy reach
|
|
49
|
+
// they do not want: Internal admits only their own domain, which is the
|
|
50
|
+
// whole set of accounts they were going to connect.
|
|
51
|
+
//
|
|
52
|
+
// What it cannot be is the only path. "Internal" is Google's word for
|
|
53
|
+
// "inside my Workspace organisation", not "private to me", and the option
|
|
54
|
+
// is simply absent on a project with no organisation behind it. So a
|
|
55
|
+
// personal @gmail.com still needs External and still has to publish, and
|
|
56
|
+
// the steps say which half applies to whom rather than making everyone read
|
|
57
|
+
// both.
|
|
43
58
|
steps: [
|
|
44
59
|
...(options.preview
|
|
45
60
|
? [
|
|
@@ -56,11 +71,14 @@ export const googleSetup = (
|
|
|
56
71
|
}\n Without gcloud: APIs & Services → Library, and search for each by name.`,
|
|
57
72
|
'The rest is under Google Auth Platform — https://console.cloud.google.com/auth — in this order:',
|
|
58
73
|
' BRANDING — app name and a support email. Seen by nobody but you.',
|
|
59
|
-
' AUDIENCE — User type
|
|
60
|
-
|
|
74
|
+
' AUDIENCE — User type, and this is the step that decides how much of the rest you do.\n INTERNAL, if the project sits in a Google Workspace organisation and every account you will connect is on that domain. An Internal app has no publishing status at all — so no seven-day expiry, no verification, no test-user list, no unverified-app warning screen, and no scopes to register. Skip to CLIENTS.\n EXTERNAL, for a personal @gmail.com, or a mix of personal and Workspace accounts. Internal is not offered without a Workspace organisation, and admits only your own domain. Add every account you will connect under "Test users", then do the two steps below.',
|
|
75
|
+
' AUDIENCE, again — EXTERNAL ONLY: PUBLISH the app. This is the setting that decides whether your connections survive the week, and it is not the same thing as verification: a client left in "Testing" has every refresh token it issues expired after exactly seven days, and one set to "In production" does not, review pending or not. Internal apps have no publishing status, which is why they never expire.',
|
|
76
|
+
` DATA ACCESS — EXTERNAL ONLY. Where scopes live now. Add:\n ${scopes.join('\n ')}\n Note drive.file is filed under "sensitive" rather than "restricted", so it appears in a different section of that page. An Internal app does not need this: it authorises scopes that were never registered here, so the page can stay empty.`,
|
|
61
77
|
' CLIENTS — Create OAuth client → type: DESKTOP APP. Google\'s docs say "Web application" with a redirect URI for Claude or Antigravity, because they assume the agent host runs the OAuth. Here the CLI does, on a loopback port — which is also why this stays Desktop even when the server runs on Cloud Run.',
|
|
62
78
|
'Copy the client ID and secret — you are asked for them next.',
|
|
63
|
-
'
|
|
79
|
+
'EXTERNAL ONLY — what publishing unverified costs, so it is a decision rather than a surprise: everyone you connect sees a "Google hasn\'t verified this app" screen and has to click through Advanced, and the project gains a cap of 100 new users granted these scopes. That cap is for the lifetime of the project and cannot be reset — which does not matter for a client only you use, and matters a great deal for one you intend to hand out. None of this applies to an Internal app.',
|
|
80
|
+
'EXTERNAL ONLY — verification itself is the other path and a much longer one: restricted scopes mean a review with a security assessment measured in months. It is worth starting and not worth waiting on, since publishing above removes the weekly re-authorisation today. If you have a Workspace domain, Internal skips this question entirely.',
|
|
81
|
+
'If none of that suits — an organisation that forbids publishing, or a client that must stay in Testing — connect with a service account key instead. It does not expire at all: lanes link connect ' + product.toLowerCase().split(' ')[0] + ' --auth service_account',
|
|
64
82
|
],
|
|
65
83
|
prompts: [
|
|
66
84
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineProvider } from '#connectivity';
|
|
2
2
|
import { DRIVE_IDENTITY, GOOGLE_APP, GOOGLE_OAUTH, specPath } from '../shared/oauth.ts';
|
|
3
|
+
import { googleServiceAccount } from '../shared/service-account.ts';
|
|
3
4
|
import { googleSetup } from '../shared/setup.ts';
|
|
4
5
|
import { SHEETS_HINTS } from './hints.ts';
|
|
5
6
|
import { SHEETS_REDACT } from './redact.ts';
|
|
@@ -60,6 +61,7 @@ export const sheets = defineProvider({
|
|
|
60
61
|
app: GOOGLE_APP,
|
|
61
62
|
scopes: SHEETS_SCOPES,
|
|
62
63
|
...GOOGLE_OAUTH,
|
|
64
|
+
assertion: googleServiceAccount('Sheets', SHEETS_SCOPES, 'optional', ['sheets.googleapis.com', 'drive.googleapis.com']),
|
|
63
65
|
},
|
|
64
66
|
identity: DRIVE_IDENTITY,
|
|
65
67
|
setup: googleSetup('Sheets', SHEETS_SCOPES, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineProvider } from '#connectivity';
|
|
2
2
|
import { GOOGLE_APP, GOOGLE_OAUTH, specPath } from '../shared/oauth.ts';
|
|
3
|
+
import { googleServiceAccount } from '../shared/service-account.ts';
|
|
3
4
|
import { googleSetup } from '../shared/setup.ts';
|
|
4
5
|
import { TASKS_REDACT } from './redact.ts';
|
|
5
6
|
|
|
@@ -47,6 +48,7 @@ export const tasks = defineProvider({
|
|
|
47
48
|
app: GOOGLE_APP,
|
|
48
49
|
scopes: TASKS_SCOPES,
|
|
49
50
|
...GOOGLE_OAUTH,
|
|
51
|
+
assertion: googleServiceAccount('Tasks', TASKS_SCOPES, 'required', ['tasks.googleapis.com']),
|
|
50
52
|
},
|
|
51
53
|
setup: googleSetup('Tasks', TASKS_SCOPES, { apis: ['tasks.googleapis.com'] }),
|
|
52
54
|
redact: TASKS_REDACT,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineLocalProvider, type ProviderDefinition } from '#connectivity';
|
|
3
|
+
import type { IdentityEntry } from '#profile';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `identity` — who the owner is, for anything written as them.
|
|
7
|
+
*
|
|
8
|
+
* **Read-only by construction, for the same reason `setup` is.** ADR-007 keeps
|
|
9
|
+
* configuration mutation off the MCP surface, and this block *is* configuration:
|
|
10
|
+
* an agent that could rewrite whose name it signs with would be rewriting the
|
|
11
|
+
* one fact that stops it signing wrong. Declaring it is the owner's, in a
|
|
12
|
+
* terminal. So there is one capability, it is a read, and `provider.test.ts`
|
|
13
|
+
* asserts the capability list holds nothing else.
|
|
14
|
+
*
|
|
15
|
+
* Reporting it authorises nothing, which is the ADR-019 argument again. A name
|
|
16
|
+
* and an address are disclosed by the first message of any mailbox this endpoint
|
|
17
|
+
* serves — a caller holding a grant on a mail connection already has them —
|
|
18
|
+
* so withholding them here while serving the mailbox would be theatre. The
|
|
19
|
+
* difference is that reading them *here* means not having to guess, and a guess
|
|
20
|
+
* is what this exists to replace.
|
|
21
|
+
*
|
|
22
|
+
* Why it is its own provider rather than a third section of `setup_overview`:
|
|
23
|
+
* policy. `identity.*` is a grant an owner can give or withhold on its own, so
|
|
24
|
+
* an endpoint can describe what is connected without naming its owner, or name
|
|
25
|
+
* its owner without describing what is connected. Folding it into `setup` would
|
|
26
|
+
* have made those one decision, and they are not.
|
|
27
|
+
*
|
|
28
|
+
* `identity.list` is clean against the seven patterns in
|
|
29
|
+
* `dispatch/control-plane.test.ts`. So is `identity` as a prefix. Neither is a
|
|
30
|
+
* cosmetic name.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export interface IdentityProviderOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Which profile this instance serves.
|
|
36
|
+
*
|
|
37
|
+
* Stamped at construction for the reason recorded on `SetupProviderOptions`:
|
|
38
|
+
* `makeHandler` strips `profile` off the arguments before dispatch and
|
|
39
|
+
* `ProviderContext` does not carry it, so a handler cannot learn it. One
|
|
40
|
+
* registry is built per profile, so each instance gets its own.
|
|
41
|
+
*/
|
|
42
|
+
readonly profile: string;
|
|
43
|
+
/**
|
|
44
|
+
* Which target it is serving, for the command in the empty case.
|
|
45
|
+
*
|
|
46
|
+
* Needed for the same reason `setup`'s plans need it: `--profile` and
|
|
47
|
+
* `--target` are required flags (ADR-037), and a command handed to an agent
|
|
48
|
+
* without both is a paste that refuses. An agent pastes what it is given.
|
|
49
|
+
*/
|
|
50
|
+
readonly target?: string;
|
|
51
|
+
/**
|
|
52
|
+
* What that profile declares, in declaration order.
|
|
53
|
+
*
|
|
54
|
+
* A snapshot rather than a function, unlike `setup`'s `reachable`. That one
|
|
55
|
+
* has to be re-evaluated per call because policy is; this is config, and the
|
|
56
|
+
* registry holding it is rebuilt whenever config is re-read — so an entry
|
|
57
|
+
* added by the CLI is served after the next reload, exactly as a new
|
|
58
|
+
* connection is, and a function here would only imply a freshness it could
|
|
59
|
+
* not deliver.
|
|
60
|
+
*/
|
|
61
|
+
readonly entries?: readonly IdentityEntry[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createIdentityProvider(options: IdentityProviderOptions): ProviderDefinition {
|
|
65
|
+
const entries = options.entries ?? [];
|
|
66
|
+
|
|
67
|
+
return defineLocalProvider({
|
|
68
|
+
id: 'identity',
|
|
69
|
+
name: 'Identity',
|
|
70
|
+
version: '1.0.0',
|
|
71
|
+
description:
|
|
72
|
+
'The names, addresses, and handles this profile declares for its owner, and a note on ' +
|
|
73
|
+
'when each applies. Read-only: what is declared is set in the CLI, because an agent able ' +
|
|
74
|
+
'to change whose name it signs with could change the one fact that stops it signing wrong.',
|
|
75
|
+
|
|
76
|
+
configSchema: z.object({}),
|
|
77
|
+
connectionSchema: z.object({}),
|
|
78
|
+
|
|
79
|
+
bundles: [
|
|
80
|
+
{
|
|
81
|
+
name: 'read',
|
|
82
|
+
description: 'Read the declared identity. There is no write bundle, by design.',
|
|
83
|
+
oauth_scopes: [],
|
|
84
|
+
capabilities: ['list'],
|
|
85
|
+
default: true,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
|
|
89
|
+
capabilities: [
|
|
90
|
+
{
|
|
91
|
+
kind: 'tool',
|
|
92
|
+
name: 'list',
|
|
93
|
+
title: 'Who the owner is, in this profile',
|
|
94
|
+
description:
|
|
95
|
+
'The names, addresses, and handles this profile declares for its owner, each with a ' +
|
|
96
|
+
'note on when it applies. Call this before writing as them — signing a message, ' +
|
|
97
|
+
'addressing one, choosing an account to act as — rather than inferring one from ' +
|
|
98
|
+
'the conversation. A profile may declare several of a kind deliberately.',
|
|
99
|
+
inputSchema: z.object({}),
|
|
100
|
+
// No arguments to redact.
|
|
101
|
+
async handler(_input, _handlerContext) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: render(options.profile, options.target, entries) }],
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Grouped by kind, in order of first appearance, and self-describing per line.
|
|
113
|
+
*
|
|
114
|
+
* Grouped because the failure this exists to prevent is a name used where an
|
|
115
|
+
* address was wanted, and adjacency is most of what stops that. The kind is
|
|
116
|
+
* repeated on every line anyway rather than written once as a heading: a line
|
|
117
|
+
* lifted out of this block on its own then still says what it is, and a model
|
|
118
|
+
* quoting one line is exactly what happens next.
|
|
119
|
+
*
|
|
120
|
+
* Order within a kind is declaration order, which is the owner's ranking.
|
|
121
|
+
*/
|
|
122
|
+
function render(
|
|
123
|
+
profile: string,
|
|
124
|
+
target: string | undefined,
|
|
125
|
+
entries: readonly IdentityEntry[],
|
|
126
|
+
): string {
|
|
127
|
+
if (entries.length === 0) {
|
|
128
|
+
// Both flags, spelled out. They are required (ADR-037), so a command
|
|
129
|
+
// missing either is one the owner pastes and watches refuse — and this is
|
|
130
|
+
// handed to an agent, which relays it verbatim.
|
|
131
|
+
const where = `--profile ${profile}${target ? ` --target ${target}` : ''}`;
|
|
132
|
+
return (
|
|
133
|
+
`Profile "${profile}" declares no identity.\n\n` +
|
|
134
|
+
'Nothing here says what name or address to use, so do not invent one — ask. ' +
|
|
135
|
+
`The owner declares them with \`lanes link identity add <kind> <value> ${where}\`.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const kinds = [...new Set(entries.map((entry) => entry.kind))];
|
|
140
|
+
const ordered = kinds.flatMap((kind) => entries.filter((entry) => entry.kind === kind));
|
|
141
|
+
|
|
142
|
+
const kindWidth = Math.max(...ordered.map((entry) => entry.kind.length));
|
|
143
|
+
const valueWidth = Math.max(...ordered.map((entry) => entry.value.length));
|
|
144
|
+
|
|
145
|
+
const lines = ordered.map((entry) => {
|
|
146
|
+
const head = ` ${entry.kind.padEnd(kindWidth)} ${entry.value}`;
|
|
147
|
+
return entry.note ? `${head.padEnd(kindWidth + valueWidth + 4)} — ${entry.note}` : head;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Said here rather than left to the reader because "several names" is
|
|
151
|
+
// otherwise ambiguous in the one direction that matters: a model handed two
|
|
152
|
+
// with no ranking picks by position anyway, and may as well be told that
|
|
153
|
+
// position is what it means.
|
|
154
|
+
const several = kinds.some((kind) => entries.filter((entry) => entry.kind === kind).length > 1);
|
|
155
|
+
|
|
156
|
+
return [
|
|
157
|
+
`Identity for profile "${profile}".`,
|
|
158
|
+
'',
|
|
159
|
+
...lines,
|
|
160
|
+
'',
|
|
161
|
+
several
|
|
162
|
+
? 'Where a kind holds more than one, the first is the default and the notes say when to ' +
|
|
163
|
+
'prefer another. If none of them fits what you are doing, ask rather than combining them.'
|
|
164
|
+
: 'Use these as written. If what you need is not here, ask rather than inferring it.',
|
|
165
|
+
].join('\n');
|
|
166
|
+
}
|
package/src/providers/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { github } from './github/index.ts';
|
|
|
6
6
|
import { drive } from './google/drive/index.ts';
|
|
7
7
|
import { driveMcp } from './google/drive-mcp/index.ts';
|
|
8
8
|
import { gmail } from './google/gmail/index.ts';
|
|
9
|
+
import { gmailImap } from './google/gmail-imap/index.ts';
|
|
9
10
|
import { gmailMcp } from './google/gmail-mcp/index.ts';
|
|
10
11
|
import { sheets } from './google/sheets/index.ts';
|
|
11
12
|
import { tasks } from './google/tasks/index.ts';
|
|
@@ -59,6 +60,7 @@ export const PROVIDERS: readonly (ProviderManifest | ProviderDefinition)[] = [
|
|
|
59
60
|
calendar,
|
|
60
61
|
tasks,
|
|
61
62
|
contacts,
|
|
63
|
+
gmailImap,
|
|
62
64
|
gmailMcp,
|
|
63
65
|
driveMcp,
|
|
64
66
|
icloudMail,
|
|
@@ -87,6 +89,7 @@ export {
|
|
|
87
89
|
drive,
|
|
88
90
|
driveMcp,
|
|
89
91
|
gmail,
|
|
92
|
+
gmailImap,
|
|
90
93
|
gmailMcp,
|
|
91
94
|
sheets,
|
|
92
95
|
tasks,
|
package/src/providers/owner.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The owner layer — memory, skills, vault.
|
|
2
|
+
* The owner layer — memory, skills, vault, setup, identity.
|
|
3
3
|
*
|
|
4
4
|
* Three providers that hold no third-party account: no OAuth, no vendor API, no
|
|
5
5
|
* rate limit anyone else imposes. They are ordinary `defineLocalProvider`
|
|
@@ -19,7 +19,14 @@
|
|
|
19
19
|
* read-only by construction — see ADR-019 for why describing setup is not one
|
|
20
20
|
* of ADR-007's control-plane exclusions.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* `identity` is the fifth and holds no account either. It says who the owner is
|
|
23
|
+
* — the names and addresses to write as them — and is read-only for the reason
|
|
24
|
+
* `setup` is: what it reports is configuration, and configuration is changed in
|
|
25
|
+
* the CLI. It is a provider of its own rather than a section of `setup` so that
|
|
26
|
+
* naming the owner and describing what is connected are two policy decisions
|
|
27
|
+
* instead of one.
|
|
28
|
+
*
|
|
29
|
+
* The ids `memory`, `skills`, `vault`, `setup`, and `identity` are reserved (`RESERVED_PROVIDER_IDS`)
|
|
23
30
|
* and still refused by default — the registry has to be built with
|
|
24
31
|
* `allowReserved` to hold them, so a third-party provider cannot claim a
|
|
25
32
|
* namespace whose policy rules would then mean something else.
|
|
@@ -29,6 +36,7 @@ export { memoryProvider, memoryStorage, assertEntryId, type MemoryEntry } from '
|
|
|
29
36
|
export { createSkillsProvider, type SkillsProviderOptions } from './skills/provider.ts';
|
|
30
37
|
export { createVaultProvider, type VaultProviderOptions } from './vault/provider.ts';
|
|
31
38
|
export { createSetupProvider, type SetupProviderOptions } from './setup/provider.ts';
|
|
39
|
+
export { createIdentityProvider, type IdentityProviderOptions } from './identity/provider.ts';
|
|
32
40
|
export { planAll, planFor, type PlanContext, type ProviderPlan } from './setup/plan.ts';
|
|
33
41
|
// The vault's *store* is not here: it is `#secrets`, beside the system
|
|
34
42
|
// credential store it must never become. What lives in `./vault/` is the
|
package/src/providers/scopes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GOOGLE_SCOPE_MEANINGS } from './google/shared/scopes.ts';
|
|
2
2
|
import { LINEAR_SCOPE_MEANINGS } from './linear/scopes.ts';
|
|
3
|
+
import { SLACK_SCOPE_MEANINGS } from './slack/scopes.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* What a scope actually permits, contributed by the provider that requests it.
|
|
@@ -23,4 +24,5 @@ export interface ScopeMeaning {
|
|
|
23
24
|
export const SCOPE_MEANINGS: Record<string, ScopeMeaning> = {
|
|
24
25
|
...GOOGLE_SCOPE_MEANINGS,
|
|
25
26
|
...LINEAR_SCOPE_MEANINGS,
|
|
27
|
+
...SLACK_SCOPE_MEANINGS,
|
|
26
28
|
};
|