@lanes-sh/link 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -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 +56 -158
- package/src/cli/commands/operate/outputs.ts +38 -11
- package/src/cli/commands/operate/serve.ts +3 -0
- 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.ts +83 -35
- package/src/cli/commands/setup.ts +22 -6
- package/src/cli/commands/target.ts +65 -83
- package/src/cli/config-edit.ts +48 -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/identity.ts +12 -1
- package/src/cli/main.ts +68 -4
- 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 +67 -49
- 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 +357 -0
- package/src/cli/usage.ts +32 -9
- 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 +48 -19
- 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 +2 -2
- package/src/deployments/servable.ts +81 -0
- package/src/deployments/target.ts +3 -2
- package/src/deployments/upload.ts +2 -1
- package/src/dispatch/dispatch.ts +1 -1
- package/src/profile/identity.ts +60 -0
- package/src/profile/index.ts +15 -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 +46 -3
- package/src/profile/targets.ts +74 -114
- package/src/profile/workspace.ts +71 -84
- 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 +5 -2
- 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
package/src/profile/workspace.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { homedir } from 'node:os';
|
|
|
6
6
|
import { parse as parseYaml } from 'yaml';
|
|
7
7
|
import { ConfigError } from './load.ts';
|
|
8
8
|
import { workspaceSchema, type Config, type WorkspaceConfig } from './schema.ts';
|
|
9
|
-
import { askedTarget } from './targets.ts';
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Workspace and profile resolution.
|
|
@@ -19,50 +18,52 @@ import { askedTarget } from './targets.ts';
|
|
|
19
18
|
* work.yaml
|
|
20
19
|
* data/ local state per profile, gitignored
|
|
21
20
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* bites is the dotfile nothing prints.
|
|
21
|
+
* **A command says which profile it means, or it does not run** (ADR-037).
|
|
22
|
+
* `--profile` is the only thing that selects one. `LANES_LINK_PROFILE` and
|
|
23
|
+
* `default_profile` are parsed and ignored.
|
|
26
24
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* from
|
|
25
|
+
* The argument this replaces was that persisted selection is how operators act
|
|
26
|
+
* on the wrong thing, and that a *visible* fallback — an exported variable, a
|
|
27
|
+
* key in a file the operator reads, and a line printed before every command —
|
|
28
|
+
* was therefore safe. The first half stands and is why this rule exists at all.
|
|
29
|
+
* What did not survive is the conclusion: the printed line is a dim grey one,
|
|
30
|
+
* and a fallback made an ignored flag survivable, so `profile add --target
|
|
31
|
+
* cloud` dropping its flag surfaced on the *next* command, from a different
|
|
32
|
+
* source, detached from its cause. A resolver with nothing to fall back to
|
|
33
|
+
* cannot do that.
|
|
34
|
+
*
|
|
35
|
+
* The workspace root is deliberately not part of this and keeps its chain —
|
|
36
|
+
* `LANES_LINK_HOME`, then an ancestor holding `lanes-link.yaml`, then
|
|
37
|
+
* `~/.lanes-link`. Getting it wrong yields "no profiles here" rather than an
|
|
38
|
+
* action against the wrong account, it is the only channel a container has for
|
|
39
|
+
* its bucket (ADR-023), and the ancestor walk is what makes a per-repository
|
|
40
|
+
* workspace work at all.
|
|
34
41
|
*/
|
|
35
42
|
|
|
36
43
|
export const WORKSPACE_FILE = 'lanes-link.yaml';
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
/** A profile, found. Everything a command needs before it has read the config. */
|
|
46
|
+
export interface ProfileSelection {
|
|
39
47
|
readonly workspaceRoot: string;
|
|
40
48
|
readonly profile: string;
|
|
41
49
|
readonly profilePath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A profile and the target whose stores a command will open.
|
|
54
|
+
*
|
|
55
|
+
* There is no `profileSource`/`targetSource` any more, and nothing should
|
|
56
|
+
* reintroduce them: with one way to select each, a source field has one
|
|
57
|
+
* inhabitant, and `announce` would print `(flag)` twice on every line of every
|
|
58
|
+
* command forever. `target.ts` already makes that argument about a line printed
|
|
59
|
+
* unconditionally — it stops being read.
|
|
60
|
+
*/
|
|
61
|
+
export interface Resolution extends ProfileSelection {
|
|
42
62
|
readonly target: string;
|
|
43
|
-
/** Where the value came from, so every command can print how it got here. */
|
|
44
|
-
readonly profileSource: 'flag' | 'environment' | 'workspace-default';
|
|
45
|
-
/**
|
|
46
|
-
* `deployable` is `deploy` choosing the only target it could have meant.
|
|
47
|
-
*
|
|
48
|
-
* It is its own source rather than reusing `config-default` because it is a
|
|
49
|
-
* different claim: the config default is what *commands* run against, and for
|
|
50
|
-
* every other command that is the local target. Printing "config-default"
|
|
51
|
-
* beside a target the config does not default to would be a lie on the one
|
|
52
|
-
* line that exists to say how the command got here.
|
|
53
|
-
*
|
|
54
|
-
* `environment` is `LANES_LINK_TARGET`, and it earns its own name for the
|
|
55
|
-
* same reason: a target chosen by a variable exported in another terminal an
|
|
56
|
-
* hour ago is the one an operator is most likely to be surprised by, and the
|
|
57
|
-
* fix — `unset` — is not the fix for a config default. `deploy` never
|
|
58
|
-
* produces it; `resolveDeployTarget` says why.
|
|
59
|
-
*/
|
|
60
|
-
readonly targetSource: 'flag' | 'environment' | 'config-default' | 'deployable';
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
export interface ResolveOptions {
|
|
64
66
|
readonly profileFlag?: string | undefined;
|
|
65
|
-
readonly targetFlag?: string | undefined;
|
|
66
67
|
readonly cwd?: string;
|
|
67
68
|
readonly env?: Record<string, string | undefined>;
|
|
68
69
|
}
|
|
@@ -172,45 +173,20 @@ export async function listProfiles(workspaceRoot: string): Promise<string[]> {
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
/**
|
|
175
|
-
*
|
|
176
|
+
* Find the profile a command names, or refuse saying what there is.
|
|
176
177
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
178
|
+
* `--profile` and nothing else. The refusal lists the workspace's profiles,
|
|
179
|
+
* because "which one" is the question it is asking, and it names
|
|
180
|
+
* `LANES_LINK_PROFILE` when that is set — the shell still configured for the old
|
|
181
|
+
* world is the single most confusing state to be in during the change, and it
|
|
182
|
+
* is self-limiting: the line disappears the moment the variable does.
|
|
180
183
|
*/
|
|
181
|
-
export async function resolveSelection(options: ResolveOptions = {}): Promise<
|
|
184
|
+
export async function resolveSelection(options: ResolveOptions = {}): Promise<ProfileSelection> {
|
|
182
185
|
const env = options.env ?? (process.env as Record<string, string | undefined>);
|
|
183
186
|
const workspaceRoot = resolveWorkspaceRoot(options);
|
|
187
|
+
const profile = options.profileFlag;
|
|
184
188
|
|
|
185
|
-
|
|
186
|
-
let profileSource: Resolution['profileSource'] = 'workspace-default';
|
|
187
|
-
|
|
188
|
-
if (options.profileFlag) {
|
|
189
|
-
profile = options.profileFlag;
|
|
190
|
-
profileSource = 'flag';
|
|
191
|
-
} else if (env['LANES_LINK_PROFILE']) {
|
|
192
|
-
profile = env['LANES_LINK_PROFILE'];
|
|
193
|
-
profileSource = 'environment';
|
|
194
|
-
} else {
|
|
195
|
-
profile = (await readWorkspace(workspaceRoot))?.default_profile;
|
|
196
|
-
profileSource = 'workspace-default';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!profile) {
|
|
200
|
-
const available = await listProfiles(workspaceRoot);
|
|
201
|
-
throw new ConfigError(
|
|
202
|
-
`No profile selected in workspace ${workspaceRoot}.\n` +
|
|
203
|
-
(available.length > 0
|
|
204
|
-
? `Available: ${available.join(', ')}\n` +
|
|
205
|
-
`Pass --profile <name>, set LANES_LINK_PROFILE, or set default_profile in ${WORKSPACE_FILE}.`
|
|
206
|
-
: `No profiles exist yet. Create one with: lanes link profile add <name> --default`),
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Provisional: the config has not been read yet, so `instance.default_target`
|
|
211
|
-
// is not available to fall back to. `resolveTarget` settles it, from the same
|
|
212
|
-
// helper, so the two cannot disagree about what beats what.
|
|
213
|
-
const asked = askedTarget(options.targetFlag, env);
|
|
189
|
+
if (!profile) throw noProfileNamed(workspaceRoot, await listProfiles(workspaceRoot), env);
|
|
214
190
|
|
|
215
191
|
const path = profilePath(workspaceRoot, profile);
|
|
216
192
|
if (!(await workspaceFiles(workspaceRoot).has(`profiles/${profile}.yaml`))) {
|
|
@@ -221,14 +197,35 @@ export async function resolveSelection(options: ResolveOptions = {}): Promise<Re
|
|
|
221
197
|
);
|
|
222
198
|
}
|
|
223
199
|
|
|
224
|
-
return {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
200
|
+
return { workspaceRoot, profile, profilePath: path };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The refusal for a command that named no profile. Exported so it can be tested. */
|
|
204
|
+
export function noProfileNamed(
|
|
205
|
+
workspaceRoot: string,
|
|
206
|
+
available: readonly string[],
|
|
207
|
+
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
|
|
208
|
+
): ConfigError {
|
|
209
|
+
if (available.length === 0) {
|
|
210
|
+
return new ConfigError(
|
|
211
|
+
`--profile is required, and ${workspaceRoot} holds no profiles yet.\n` +
|
|
212
|
+
' Create one with: lanes link profile add <name> --target local',
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const stale = env['LANES_LINK_PROFILE'];
|
|
217
|
+
|
|
218
|
+
return new ConfigError(
|
|
219
|
+
'--profile is required. Every command names the profile it acts on, and\n' +
|
|
220
|
+
'nothing else selects one.\n\n' +
|
|
221
|
+
` Profiles in ${workspaceRoot}\n` +
|
|
222
|
+
available.map((name) => ` ${name}`).join('\n') +
|
|
223
|
+
`\n\n e.g. lanes link status --profile ${available[0]} --target <target>` +
|
|
224
|
+
(stale
|
|
225
|
+
? `\n\n LANES_LINK_PROFILE=${stale} is set in this shell and is no longer read.\n` +
|
|
226
|
+
' Unset it, or pass --profile.'
|
|
227
|
+
: ''),
|
|
228
|
+
);
|
|
232
229
|
}
|
|
233
230
|
|
|
234
231
|
/**
|
|
@@ -250,13 +247,3 @@ export function workspacePath(workspaceRoot: string, path: string): string {
|
|
|
250
247
|
}
|
|
251
248
|
return isAbsolute(path) ? path : resolve(workspaceRoot, path);
|
|
252
249
|
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* The line every command prints before acting, read-only commands included.
|
|
256
|
-
*
|
|
257
|
-
* This is the primary guard against operating on the wrong instance, and it
|
|
258
|
-
* costs one line.
|
|
259
|
-
*/
|
|
260
|
-
export function describeSelection(resolution: Resolution): string {
|
|
261
|
-
return `profile: ${resolution.profile} (${resolution.profileSource}) target: ${resolution.target} (${resolution.targetSource})`;
|
|
262
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineProvider } from '#connectivity';
|
|
2
2
|
import { CALENDAR_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 { CALENDAR_REDACT } from './redact.ts';
|
|
5
6
|
|
|
@@ -53,6 +54,7 @@ export const calendar = defineProvider({
|
|
|
53
54
|
app: GOOGLE_APP,
|
|
54
55
|
scopes: CALENDAR_SCOPES,
|
|
55
56
|
...GOOGLE_OAUTH,
|
|
57
|
+
assertion: googleServiceAccount('Calendar', CALENDAR_SCOPES, 'optional', ['calendar-json.googleapis.com']),
|
|
56
58
|
},
|
|
57
59
|
identity: CALENDAR_IDENTITY,
|
|
58
60
|
setup: googleSetup('Calendar', CALENDAR_SCOPES, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineProvider } from '#connectivity';
|
|
2
2
|
import { GOOGLE_APP, GOOGLE_OAUTH, PEOPLE_IDENTITY, specPath } from '../shared/oauth.ts';
|
|
3
|
+
import { googleServiceAccount } from '../shared/service-account.ts';
|
|
3
4
|
import { googleSetup } from '../shared/setup.ts';
|
|
4
5
|
import { CONTACTS_REDACT } from './redact.ts';
|
|
5
6
|
|
|
@@ -43,6 +44,7 @@ export const contacts = defineProvider({
|
|
|
43
44
|
app: GOOGLE_APP,
|
|
44
45
|
scopes: CONTACTS_SCOPES,
|
|
45
46
|
...GOOGLE_OAUTH,
|
|
47
|
+
assertion: googleServiceAccount('Contacts', CONTACTS_SCOPES, 'required', ['people.googleapis.com']),
|
|
46
48
|
},
|
|
47
49
|
identity: PEOPLE_IDENTITY,
|
|
48
50
|
setup: googleSetup('Contacts', CONTACTS_SCOPES, { apis: ['people.googleapis.com'] }),
|
|
@@ -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
|
|
|
5
6
|
/**
|
|
@@ -32,6 +33,7 @@ export const docs = defineProvider({
|
|
|
32
33
|
app: GOOGLE_APP,
|
|
33
34
|
scopes: DOCS_SCOPES,
|
|
34
35
|
...GOOGLE_OAUTH,
|
|
36
|
+
assertion: googleServiceAccount('Docs', DOCS_SCOPES, 'optional', ['docs.googleapis.com', 'drive.googleapis.com']),
|
|
35
37
|
},
|
|
36
38
|
identity: DRIVE_IDENTITY,
|
|
37
39
|
setup: googleSetup('Docs', DOCS_SCOPES, {
|
|
@@ -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 { DRIVE_HINTS } from './hints.ts';
|
|
5
6
|
import { DRIVE_REDACT } from './redact.ts';
|
|
@@ -26,6 +27,7 @@ export const drive = defineProvider({
|
|
|
26
27
|
app: GOOGLE_APP,
|
|
27
28
|
scopes: DRIVE_SCOPES,
|
|
28
29
|
...GOOGLE_OAUTH,
|
|
30
|
+
assertion: googleServiceAccount('Drive', DRIVE_SCOPES, 'optional', ['drive.googleapis.com']),
|
|
29
31
|
},
|
|
30
32
|
identity: DRIVE_IDENTITY,
|
|
31
33
|
setup: googleSetup('Drive', DRIVE_SCOPES),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineProvider, defineProviderWithCapabilities } from '#connectivity';
|
|
2
2
|
import { GMAIL_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 { GMAIL_HOST } from './api.ts';
|
|
5
6
|
import { GMAIL_HINTS } from './hints.ts';
|
|
@@ -85,6 +86,7 @@ const manifest = defineProvider({
|
|
|
85
86
|
app: GOOGLE_APP,
|
|
86
87
|
scopes: GMAIL_SCOPES,
|
|
87
88
|
...GOOGLE_OAUTH,
|
|
89
|
+
assertion: googleServiceAccount('Gmail', GMAIL_SCOPES, 'required', ['gmail.googleapis.com'], 'gmail_imap'),
|
|
88
90
|
},
|
|
89
91
|
// The REST API rather than the MCP server: it answers with the address under
|
|
90
92
|
// scopes we already hold, so labelling a connection costs no extra consent.
|
|
@@ -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
|
+
});
|
|
@@ -56,11 +56,14 @@ export const googleSetup = (
|
|
|
56
56
|
}\n Without gcloud: APIs & Services → Library, and search for each by name.`,
|
|
57
57
|
'The rest is under Google Auth Platform — https://console.cloud.google.com/auth — in this order:',
|
|
58
58
|
' BRANDING — app name and a support email. Seen by nobody but you.',
|
|
59
|
-
' AUDIENCE — User type: EXTERNAL (even with a Workspace domain: "Internal" admits only that one domain, so a mix of personal and Workspace accounts needs External). Add every account you will connect under "Test users".
|
|
59
|
+
' AUDIENCE — User type: EXTERNAL (even with a Workspace domain: "Internal" admits only that one domain, so a mix of personal and Workspace accounts needs External). Add every account you will connect under "Test users".',
|
|
60
|
+
' AUDIENCE, again — 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.',
|
|
60
61
|
` DATA ACCESS — 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.`,
|
|
61
62
|
' 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
63
|
'Copy the client ID and secret — you are asked for them next.',
|
|
63
|
-
'
|
|
64
|
+
'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.',
|
|
65
|
+
'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: publishing above removes the weekly re-authorisation today.',
|
|
66
|
+
'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
67
|
],
|
|
65
68
|
prompts: [
|
|
66
69
|
{
|
|
@@ -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,
|