feat: Google Drive access for agents (provider connection) + connection auth helpers - #160
Conversation
Add @junejs/core/google-drive: googleDriveTools(config) returns a set of
defineActions (list/find/read/create/update/save/create_folder/delete) that give
a June agent the ability to read from and write to Google Drive. Because they're
ordinary actions they're simultaneously an agent tool, a UI server action, and
an /mcp tool. Pure + fetch-only (edge-safe), so Drive access works on the native
host and in a Durable Object alike.
Identity mirrors connections: the OAuth2 access token is resolved per call,
server-side via auth(ctx) — it never reaches the model — so a multi-tenant app
mints the caller's short-lived token. requiresPrincipal hides the tools from
anonymous turns. Drive's quirks are handled honestly: multipart content upload,
alt=media downloads, export for Google-native docs, and slash-path resolution
over the folder graph (mkdir -p on save).
A tools/*.ts file may now default-export one tool OR an array of tools;
defineAgent/assembleDurable flatten arrays (native discovery + edge-compiled
module), so the whole integration drops into agent/tools/ as a single
`export default googleDriveTools({ ... })`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reframe the Drive integration from a standalone tools module into the
connections family, where an agent's outbound edges belong. Add a third
connection kind alongside mcp/openapi: defineProviderConnection({ name, connect,
requiresPrincipal? }). A provider brings its own transport — connect(ctx?)
returns its tools as defineActions — for remotes the generic mcp/openapi clients
can't express (multipart uploads, alt=media downloads, compound path→id ops). It
still joins the lifecycle: connectAll reports it (kind "provider"), isolates its
failures, the durable/edge target wires it lazily, and requiresPrincipal stamps
every exposed tool.
googleDriveConnection(config) is the first provider; googleDriveTools(config)
stays exported for programmatic defineAgent use. The Drive client is unchanged.
Coverage: google-drive.ts 100% lines/funcs, connections.ts 100% funcs. New
tests cover the provider kind (sync/async connect, report, error isolation,
requiresPrincipal, synthetic url, mixed kinds) and Drive-through-connectAll
(shape, end-to-end save/read, custom name, requiresPrincipal, per-call auth),
plus previously-uncovered Drive tools (create_file via folderPath, create_folder,
update_file) and the non-JSON error fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document how the auth token is obtained: OAuth 2.0 (user) vs Service Account, which to pick, and the simplest way to get credentials (OAuth Playground for dev, a Service Account JSON key for a single shared account, an OAuth client for multi-user). Add the web-native path — because June is a web framework with a resolved request principal, the Better Auth integration can own the Google OAuth consent/callback/token-storage flow, so auth(ctx) just reads the caller's linked Google token. Add a design note: the auth seam stays overridable; a blessed helper belongs at the @junejs/server layer (not pure core), connection-agnostic and fail-closed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add host-layer helpers that turn an authenticated principal into a per-call
bearer token for an outbound connection (provider/mcp/openapi), resolved
server-side so the token never reaches the model:
- linkedAccountAuth({ providerId, store }): the generic, auth-library-agnostic
core — inject an AccountTokenStore and get a fail-closed auth(ctx) that mints
the caller's token. Connection-agnostic (Notion/Slack/GitHub/Drive alike).
- betterAuthAccessToken / betterAuthAccountTokenStore: the Better Auth
convenience, kept structural (BetterAuthLike) so it adds NO better-auth
dependency. A Service Account / raw-token user pays nothing (opt-in exports).
Lives in @junejs/server by design (the auth integration is not core's job); pure
logic, no node:*. 100% covered; server suite green (501 pass). Updated the Drive
doc to use the shipped helper.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds Google Drive read/write capabilities through a new provider connection model and introduces per-principal connection authentication helpers.
Changes:
- Adds provider connections and Google Drive actions.
- Supports array exports from tool modules.
- Adds Better Auth helpers, tests, documentation, and release metadata.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
packages/june/test/connection-auth.test.ts |
Tests connection authentication helpers. |
packages/june/src/index.ts |
Exports authentication APIs. |
packages/june/src/connection-auth.ts |
Implements linked-account token resolution. |
packages/june/src/agent-discover.ts |
Discovers array tool exports. |
packages/core/test/purity.test.ts |
Verifies Google Drive subpath resolution. |
packages/core/test/google-drive.test.ts |
Tests Drive tools and connection behavior. |
packages/core/test/connections.test.ts |
Tests provider connections. |
packages/core/test/agent-config.test.ts |
Tests tool-array flattening. |
packages/core/src/google-drive.ts |
Implements Google Drive actions and transport. |
packages/core/src/connections.ts |
Adds the provider connection kind. |
packages/core/src/agent-config.ts |
Flattens tool-array entries during assembly. |
packages/core/package.json |
Exports the Google Drive module. |
docs/google-drive-integration.md |
Documents configuration and authorization. |
.changeset/connection-auth-helpers.md |
Records the server authentication API release. |
.changeset/agent-google-drive.md |
Records Drive and provider connection releases. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // Uniform gate: `requiresPrincipal` on the connection stamps every exposed | ||
| // action — the same guarantee connectMcp/connectOpenapi give. Idempotent if a | ||
| // provider already stamped its own actions. | ||
| if (c.requiresPrincipal) for (const a of actions) a.requiresPrincipal = true; |
| // Build an `auth(ctx)` for ANY June connection that mints the CALLER's | ||
| // linked-account token. FAIL CLOSED: a missing principal or an unlinked account | ||
| // throws — so an authenticated-but-unlinked user gets a clear error, and | ||
| // (together with the connection's `requiresPrincipal`, which hides the tools from | ||
| // anonymous turns) the capability is never reachable without a real credential. | ||
| export function linkedAccountAuth(opts: LinkedAccountAuthOptions): ConnectionAuth { |
| let parent = "root"; | ||
| if (segments.length > 1) { | ||
| try { | ||
| parent = await resolveFolderPath(segments.slice(0, -1), { create: false }, ctx); | ||
| } catch { | ||
| return null; // an intermediate folder doesn't exist ⇒ the file can't | ||
| } | ||
| } |
| // Adapt a Better-Auth-shaped instance into an AccountTokenStore. | ||
| export function betterAuthAccountTokenStore(auth: BetterAuthLike): AccountTokenStore { | ||
| return async ({ userId, providerId }) => { | ||
| const res = await auth.api.getAccessToken({ providerId, userId }); |
Adopt all four reviewer findings:
1. Provider requiresPrincipal is now applied at REGISTRATION, not by post-hoc
mutation. defineAction snapshots the gate into the Flight server-reference
wrapper, so retro-setting action.requiresPrincipal left that path ungated.
ProviderConnection.connect now receives { requiresPrincipal } and must thread
it into its defineActions; connectAll fail-fast verifies every tool is gated
(never mutates). googleDriveConnection threads it through to googleDriveTools.
2. linkedAccountAuth's compatibility claim narrowed: it is for connections whose
auth resolves per call with identity (providers). MCP/OpenAPI call auth at
discovery with ctx=undefined, which this fail-closed helper rejects — documented
in code + docs.
3. resolvePathToFile no longer masks HTTP errors. It walks with findChild (null
only for a genuinely absent segment); 401/403/429/5xx now propagate instead of
surfacing as { found: false } / a misleading missing-file error. Dead
create:false branch of resolveFolderPath removed.
4. betterAuthAccountTokenStore uses Better Auth's real endpoint shape
{ body: { providerId, userId } } (was passing fields top-level); BetterAuthLike,
the call, and the fake test updated.
Coverage: google-drive.ts & connection-auth.ts 100%/100%. New tests: provider
gate-at-registration + fail-fast on a non-compliant provider, HTTP-error
propagation through path resolution, and the getAccessToken body shape. Full
suites green (core 483, server 502).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Suppressed comments (7)
packages/core/src/google-drive.ts:150
- Drive permits multiple children with the same name, so taking
files[0]makes path resolution arbitrary. In particular,save_filecan overwrite an unrelated duplicate. Fetch two matches and fail on ambiguity rather than silently selecting one.
const { files } = await listFiles({ q: clauses.join(" and "), pageSize: 1 }, ctx);
return files[0] ?? null;
packages/core/src/google-drive.ts:420
save_fileuses a non-atomic find-then-create sequence. Two concurrent identical calls can both see no file and create duplicates (Drive does not enforce unique sibling names), so advertisingidempotentHintcan cause clients to retry this operation as though it were safe. Remove the hint unless the implementation gains an idempotency/serialization mechanism.
annotations: { title: "Save Drive file (upsert)", idempotentHint: true },
packages/core/src/google-drive.ts:39
- There is no configurable base folder for path operations; both path walkers hard-code Drive's
root. This makes the documented service-account setup (share a target folder or Shared Drive) unable to usesave_file/find_file/path reads, because that shared location is not the service account's My Drive root. Add a root folder/drive ID to the config and use it as the initial parent for both readers and writers.
export type GoogleDriveConfig = {
// Tool id prefix — the tools are `<name>__read_file`, `<name>__save_file`, …
// mirroring the connection naming convention. Default "gdrive".
name?: string;
// Resolves an OAuth2 access token (scope drive / drive.file) for the call.
packages/core/src/google-drive.ts:324
- The tool description says the return value is metadata or
null, but the implementation returns{ found, file }. Since this description is shown to the model, callers are given the wrong result contract. Describe the wrapper shape used by the implementation.
description:
"Find a file or folder by its slash-separated path from the Drive root, e.g. 'Reports/2024/summary.txt'. Returns the file's metadata (id, name, mimeType, ...) or null when it does not exist.",
docs/google-drive-integration.md:93
- This
assembleAgent(mod)example does not type-check becauseAgentModulerequiressurfaceInstructions, but the object omits it. Include an empty map as the other optional convention fields are already doing.
const mod = {
config: { name: "archivist" },
instructions: "Save your outputs to Drive and read reference docs from it.",
tools: [], skills: [], channels: {}, channelInstructions: {},
connections: [googleDriveConnection({ auth: () => ({ token: process.env.GOOGLE_DRIVE_TOKEN! }) })],
};
const agent = await assembleAgent(mod);
.changeset/agent-google-drive.md:11
- The release note documents
connect(ctx?), but the new API actually callsconnect({ requiresPrincipal }); no action context is passed. Update the signature so consumers do not implement the provider contract incorrectly.
`defineProviderConnection({ name, connect, requiresPrincipal? })`. A provider
brings its OWN transport — `connect(ctx?)` returns the provider's tools as
`defineAction`s — for remotes the generic mcp/openapi clients can't express
docs/google-drive-integration.md:45
- The implementation returns
{ found, file }, not metadata ornulldirectly. Update this table entry to match the tool's actual result contract.
| `gdrive__find_file` | Resolve a slash path (`A/B/file.txt`) → metadata, or `null`. |
| const actions = await c.connect({ requiresPrincipal: c.requiresPrincipal }); | ||
| // Fail fast (never retro-mutate): if the connection demands a principal, every | ||
| // tool must already be gated. A non-compliant provider is a bug, and mutating | ||
| // here would leave the Flight path ungated while looking safe. | ||
| if (c.requiresPrincipal) { |
…ess)
Main finding (transactional registration): connection tools self-register in the
global ACTION_REGISTRY (defineAction), which /mcp lists and invokeAction
dispatches. A connection that failed partway — a provider throwing after
registering, or the gate-check rejecting an ungated tool — left those actions
reachable even though the connection was "skipped".
- connectAll now snapshots the registry per connection and ROLLS BACK everything
a failed connection added (all kinds: mcp/openapi/provider).
- The Flight server-reference wrapper resolves the action from ACTION_REGISTRY at
call time and gates off the LIVE action, so a rolled-back action's reference is
inert (deleting the map entry is now sufficient) and the gate can never be
stale.
Google Drive correctness (from suppressed comments):
- rootFolderId config: path walkers + create default their base to it, so the
documented service-account "share a folder / Shared Drive" setup actually works
(a service account's My Drive root is empty/unwritable).
- findChild fails on ambiguous same-name siblings (Drive allows them) instead of
arbitrarily taking files[0] — save_file can no longer overwrite an unrelated
duplicate.
- save_file drops idempotentHint (non-atomic find-then-create can race).
- find_file description now states its real { found, file } contract.
Docs/changeset: connect({ requiresPrincipal }) signature, find_file contract,
rootFolderId for the SA setup, complete assembleAgent example.
Coverage: google-drive.ts 100%, connections.ts 100% funcs. Suites green
(core 490, server 502).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
packages/core/src/connections.ts:263
- This global before/after diff spans an awaited connection attempt. If another
connectAll/agent assembly registers tools while this attempt is pending, a later failure deletes those unrelated tools because their IDs were absent from this snapshot. Rollback needs connection-owned registration tracking or global transaction serialization rather than deleting every concurrently added registry entry.
for (const key of ACTION_REGISTRY.keys()) if (!before.has(key)) ACTION_REGISTRY.delete(key);
packages/core/src/connections.ts:255
- The rollback snapshots only IDs, so it cannot restore an existing action overwritten by the failed connection. A provider can register an action under a pre-existing ID and then throw; because that ID is in
before, rollback leaves the failed provider's action globally reachable instead of restoring the original. Snapshot the entries and restore prior values as well as deleting newly added IDs.
const before = new Set(ACTION_REGISTRY.keys());
.changeset/agent-google-drive.md:27
- The release note advertises
idempotentHint, butsave_filedeliberately does not set it and the new test asserts that it is undefined. Correct the changeset so the published changelog matches the shipped API.
mints the caller's short-lived token. Read/list/find carry `readOnlyHint`,
`save_file` carries `idempotentHint`, `delete_file` carries `destructiveHint`.
.changeset/connection-auth-helpers.md:15
- “Reusable across every connection kind” is unsafe for MCP/OpenAPI remotes that authenticate discovery: those paths call
auth(undefined), while this helper always rejects without a principal (asconnection-auth.ts:15-19documents). Qualify the release note so users do not wire this helper into discovery-authenticated connections and silently lose all tools.
unreachable without a real credential). Reusable across every connection kind,
not just Google Drive.
docs/google-drive-integration.md:54
- This guide says
save_fileadvertisesidempotentHint, but the implementation and test intentionally omit it because the upsert is non-atomic (packages/core/src/google-drive.ts:439-442). Update the permission-UX documentation so clients are not told to expect an annotation that is absent.
Read/list/find carry `readOnlyHint`; `save_file` carries `idempotentHint`;
`delete_file` carries `destructiveHint` — so MCP clients can drive permission UX.
packages/core/src/google-drive.ts:219
- The prefix also matches non-exportable Drive resources such as folders and shortcuts, but they are not in
EXPORT_MIME; the fallback consequently sends an invalidfiles.export(..., mimeType=text/plain)request. Export only explicitly supported native types and reject unsupported Google-native resources before attemptingalt=media.
if (file.mimeType.startsWith("application/vnd.google-apps.")) {
const exportMime = EXPORT_MIME[file.mimeType] ?? "text/plain";
const search = new URLSearchParams({ mimeType: exportMime });
const res = await request(`${api}/files/${encodeURIComponent(file.id)}/export?${search}`, { method: "GET" }, ctx);
| const flightRun = (...args: unknown[]) => { | ||
| const current = ACTION_REGISTRY.get(def.id); | ||
| if (!current) throw new Error(`Action "${def.id}" is not registered`); | ||
| if (current.requiresPrincipal) { | ||
| const ctx = args[1] as ActionContext | undefined; | ||
| if (!ctx?.user) throw new Error(`Action "${def.id}" requires an authenticated principal (ctx.user)`); | ||
| } | ||
| return (current.run as (...a: unknown[]) => unknown)(...args); | ||
| }; |
…entity)
- Flight reference identity binding (agent.ts): the server reference is bound to
the EXACT action instance — it runs only while ACTION_REGISTRY still maps its id
to that action. A rollback (delete) OR a same-id overwrite by a different action
both make a stale reference inert, so it can never alias onto a replacement and
run the wrong semantics.
- Concurrency- and overwrite-safe connection rollback (connections.ts):
* connectAll is now serialized globally (per isolate). The registry is shared,
so overlapping agent-assembly runs could otherwise interleave registrations
and a failure's snapshot-diff would delete another run's tools. Serializing
the boot-time/first-turn wiring makes each run's snapshot a faithful baseline
(cheap; separate DO isolates have separate registries).
* rollback now snapshots ENTRIES and both deletes ids the failed connection
added AND restores any pre-existing entry it overwrote — a failed connection
that clobbered an id no longer leaves its action reachable under that id.
- Drive read guards non-exportable Google-native resources (folders, shortcuts,
…): reject with a clear error instead of firing a bogus files.export.
- Docs/changesets: save_file has no idempotentHint; linkedAccountAuth is for
per-call-identity connections, not discovery-authenticated MCP/OpenAPI.
Tests: Flight inert on delete AND overwrite; rollback restores an overwritten
pre-existing action; connectAll serialization; read of a folder rejected.
google-drive.ts 100%. Suites green (core 493, server 502).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (5)
packages/core/src/google-drive.ts:223
- Every non-Google-native object is decoded with
Response.text(), including PDFs, images, archives, and arbitrarily large files.read_filetherefore returns corrupted replacement-character text for binary files and can exhaust an edge isolate while buffering a large Drive object. Restrict this path to supported textual MIME types and enforce a configurable byte limit, or expose an explicit bounded binary/base64 mode.
const search = new URLSearchParams({ alt: "media", supportsAllDrives: "true" });
const res = await request(`${api}/files/${encodeURIComponent(file.id)}?${search}`, { method: "GET" }, ctx);
return res.text();
packages/core/src/google-drive.ts:447
- The PR description still promises
idempotentHintonsave_file, while this implementation deliberately omits it because the upsert is non-atomic. The omission is safer; update the PR description so consumers are not told this operation is retry-safe.
// No idempotentHint: the upsert is a non-atomic find-then-create/update, so
// two concurrent identical saves can race into duplicate files (Drive does
// not enforce unique sibling names). Don't advertise safe-to-retry.
annotations: { title: "Save Drive file (upsert)" },
packages/june/src/connection-auth.ts:90
- The real Better Auth
getAccessTokenendpoint throwsACCOUNT_NOT_FOUNDwhen this user/provider has no account; it does not returnnullas the fake does. ConsequentlybetterAuthAccessTokenbypasseslinkedAccountAuth's documented “no linked account” path and leaks a Better Auth API error instead. Normalize only the account-not-found error tonullhere (while rethrowing refresh/network failures), and cover that behavior with a throwing fake.
const res = await auth.api.getAccessToken({ body: { providerId, userId } });
packages/core/src/connections.ts:263
- This module-global queue is held while MCP/OpenAPI discovery performs network fetches with no timeout. If one endpoint never settles, every later
connectAllcall remains queued, preventing unrelated agents or durable instances in the same isolate from initializing. Add bounded cancellation and/or redesign the registry transaction so network discovery does not hold a global lock.
const result = connectAllQueue.then(run, run); // chain regardless of the prior run's outcome
packages/core/src/google-drive.ts:109
authHeaderis resolved for every Drive HTTP request, not once per tool call as the API contract states. Compound operations such assave_fileperform several path lookups/creates, so the Better Auth adapter can repeat the account lookup/refresh many times and even switch tokens mid-operation. Resolve the credential once at action entry and reuse it for all requests in that invocation.
This issue also appears in the following locations of the same file:
- line 221
- line 444
headers.set("authorization", await authHeader(ctx));
Adopt all four reviewer findings:
1. Provider requiresPrincipal is now applied at REGISTRATION, not by post-hoc
mutation. defineAction snapshots the gate into the Flight server-reference
wrapper, so retro-setting action.requiresPrincipal left that path ungated.
ProviderConnection.connect now receives { requiresPrincipal } and must thread
it into its defineActions; connectAll fail-fast verifies every tool is gated
(never mutates). googleDriveConnection threads it through to googleDriveTools.
2. linkedAccountAuth's compatibility claim narrowed: it is for connections whose
auth resolves per call with identity (providers). MCP/OpenAPI call auth at
discovery with ctx=undefined, which this fail-closed helper rejects — documented
in code + docs.
3. resolvePathToFile no longer masks HTTP errors. It walks with findChild (null
only for a genuinely absent segment); 401/403/429/5xx now propagate instead of
surfacing as { found: false } / a misleading missing-file error. Dead
create:false branch of resolveFolderPath removed.
4. betterAuthAccountTokenStore uses Better Auth's real endpoint shape
{ body: { providerId, userId } } (was passing fields top-level); BetterAuthLike,
the call, and the fake test updated.
Coverage: google-drive.ts & connection-auth.ts 100%/100%. New tests: provider
gate-at-registration + fail-fast on a non-compliant provider, HTTP-error
propagation through path resolution, and the getAccessToken body shape. Full
suites green (core 483, server 502).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ess)
Main finding (transactional registration): connection tools self-register in the
global ACTION_REGISTRY (defineAction), which /mcp lists and invokeAction
dispatches. A connection that failed partway — a provider throwing after
registering, or the gate-check rejecting an ungated tool — left those actions
reachable even though the connection was "skipped".
- connectAll now snapshots the registry per connection and ROLLS BACK everything
a failed connection added (all kinds: mcp/openapi/provider).
- The Flight server-reference wrapper resolves the action from ACTION_REGISTRY at
call time and gates off the LIVE action, so a rolled-back action's reference is
inert (deleting the map entry is now sufficient) and the gate can never be
stale.
Google Drive correctness (from suppressed comments):
- rootFolderId config: path walkers + create default their base to it, so the
documented service-account "share a folder / Shared Drive" setup actually works
(a service account's My Drive root is empty/unwritable).
- findChild fails on ambiguous same-name siblings (Drive allows them) instead of
arbitrarily taking files[0] — save_file can no longer overwrite an unrelated
duplicate.
- save_file drops idempotentHint (non-atomic find-then-create can race).
- find_file description now states its real { found, file } contract.
Docs/changeset: connect({ requiresPrincipal }) signature, find_file contract,
rootFolderId for the SA setup, complete assembleAgent example.
Coverage: google-drive.ts 100%, connections.ts 100% funcs. Suites green
(core 490, server 502).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What & why
Gives June-built agents the ability to read from and write to Google Drive — an agent can save a file it produced, or read a file/path from Drive. Scenarios: "agent generated a report → store it in Drive", "read a reference doc from a known path".
Rather than a one-off tools module, Drive lands in the connections family (an agent's outbound edges), with a small, principled extension so it fits cleanly.
How it's built
1. New
providerconnection kind (@junejs/core/connections)The existing
mcp/openapikinds wire a generic remote. Drive's REST API has quirks neither can express — multipart content upload,alt=mediadownloads,exportfor Google-native docs, and path→id resolution over the folder graph (save_fileis a compound resolve→find→create/update). So a third kind,defineProviderConnection({ name, connect, requiresPrincipal? }), lets a remote bring its own transport while still joining the connection lifecycle:connectAllreports it, isolates its failures, the durable/edge target wires it lazily, andrequiresPrincipalstamps every exposed tool.2. Google Drive integration (
@junejs/core/google-drive)googleDriveConnection(config)is the first provider — tools:list_files,find_file,read_file,create_file,update_file,save_file(upsert-by-path),create_folder,delete_file. Each is adefineAction, so it's simultaneously an agent tool, a UI server action, and an/mcptool.googleDriveTools(config)is also exported for programmaticdefineAgent. Pure +fetch-only (edge-safe, nonode:*). The OAuth2 token is resolved per call, server-side viaauth(ctx)— it never reaches the model. MCP annotations:readOnlyHinton reads,idempotentHintonsave_file,destructiveHintondelete_file.3. Directory convention: array tool exports
A
tools/*.tsfile may now default-export one tool or an array;defineAgent/assembleDurableflatten arrays (native discovery + edge-compiled module).4. Connection auth helpers (
@junejs/server)Turn an authenticated principal into a per-call token for any connection:
linkedAccountAuth({ providerId, store })— generic, auth-library-agnostic, fail closed (no principal / unlinked account throws).betterAuthAccessToken(auth, { providerId })/betterAuthAccountTokenStore(auth)— the blessed Better Auth convenience, structural (BetterAuthLike) so it adds nobetter-authdependency. Service Account / raw-token users pay nothing (pure opt-in exports).This leans on June's web nature: the awkward OAuth redirect/callback/token-storage dance is owned by the (blessed) Better Auth integration, so
auth(ctx)just reads the caller's linked Google token.Tests & coverage
google-drive.ts: 100% lines/funcs (in-memory Drive fake: query subset, multipart/media upload, export/alt=media).connections.ts: provider kind fully covered (sync/async connect, report, error isolation,requiresPrincipal, synthetic url, mixed kinds).connection-auth.ts: 100% (fail-closed paths, structural Better Auth adapter, SA path bypass).tscclean (core + server); core 476 pass, server 501 pass, 0 fail.Docs & changesets
docs/google-drive-integration.md— full guide: provider-kind rationale, tools, usage (connection + programmatic), authorization models (OAuth vs Service Account), how to obtain credentials, the web-native Better Auth flow, and the shipped helper.@junejs/coreminor +@junejs/serverminor.