basalt / drives-google/src / GoogleDrive
Class: GoogleDrive
Defined in: drives-google/src/index.ts:216
The adapter. Construct it with googleDrive.
Implements
Constructors
Constructor
> new GoogleDrive(options): GoogleDrive
Defined in: drives-google/src/index.ts:253
Parameters
options
Returns
GoogleDrive
Properties
allowedHosts
> readonly allowedHosts: readonly string[]
Defined in: drives-google/src/index.ts:226
The SSRF allowlist.
accounts.google.com is not on it: the consent URL is handed to a browser and never fetched by the framework, so allowing the host would widen the guard for nothing. .googleusercontent.com is on it because the download redirect genuinely lands there — see CDN_SUFFIX in this module.
Implementation of
authorization
> readonly authorization: DriveAuthorization
Defined in: drives-google/src/index.ts:243
Implementation of
deltaIncludesExisting
> readonly deltaIncludesExisting: false = false
Defined in: drives-google/src/index.ts:242
false, and this is the single most consequential line in the adapter.
changes.getStartPageToken is documented as the token for "the start of the future": the corpus that already exists never appears in changes.list. With true — or with the field left off against an engine that defaulted the other way — a connection's first sync would walk an empty change feed, report success, import nothing, and persist a cursor that guarantees the existing files are never seen again.
With false the engine takes the start token first, then runs a full listing pass, then switches to the feed. The ordering is the correctness argument: anything that changes during the listing is re-delivered by the first delta run, which is at-least-once and the ledger absorbs it.
Implementation of
DriveProvider.deltaIncludesExisting
name
> readonly name: "google" = 'google'
Defined in: drives-google/src/index.ts:217
Stable identifier, stored on every connection: google, microsoft, dropbox.
Implementation of
Methods
delta()
> delta(session, cursor): Promise<DriveDelta>
Defined in: drives-google/src/index.ts:652
One page of the change feed.
Two things make this the least mechanical method in the adapter:
The feed is account-wide. A connection confined to a rootId sees every change in the user's Drive, so each one is scope-checked by walking its ancestry (parents, then a metadata read per unseen folder, cached for the duration of this one call). Out-of-scope changes are dropped before they become a DriveChange, so nothing about another folder reaches onRemoved, the hooks or the ledger.
A hard deletion carries no file resource. {fileId, removed: true} is all there is, so for a scoped connection there is nothing to test the ancestry of. Those are dropped by default (see GoogleDriveOptions.includeUnscopedRemovals); the ordinary Drive delete — a trash — arrives as a full resource with trashed: true and is scoped like anything else.
Parameters
session
cursor
string
Returns
Promise<DriveDelta>
Implementation of
download()
> download(session, item): Promise<DriveContent>
Defined in: drives-google/src/index.ts:524
Opens an item's bytes.
Streamed, never buffered. The interesting part is the hop: alt=media answers 302 to a signed URL on *.googleusercontent.com, and the guarded fetch re-runs the host allowlist, the SSRF validation and the IP pin on that target before a byte moves. The adapter never sees the signed URL and therefore cannot log it, which matters because that URL is a bearer credential for the file.
Parameters
session
item
Returns
Promise<DriveContent>
Implementation of
get()
> get(session, externalId): Promise<DriveItem | null>
Defined in: drives-google/src/index.ts:489
Fetches one item's metadata. null when it no longer exists.
Parameters
session
externalId
string
Returns
Promise<DriveItem | null>
Implementation of
list()
> list(session, options): Promise<DrivePage<DriveItem>>
Defined in: drives-google/src/index.ts:424
One page of a listing.
Drive has no recursive query. '<id>' in parents returns a folder's direct children and nothing deeper, and there is no under: operator to ask for a subtree — so an adapter that stopped there would give a rootId-scoped connection a backfill containing only the top level, while the change feed (which is account-wide, and filtered by ancestry) happily delivered the subfolders' files afterwards. The first sync would silently miss most of a tenant's documents and the second would look like it was inventing them.
So a scoped listing walks: one folder at a time, pushing the folders it finds onto a queue carried inside the cursor. The cursor is opaque to everything above the adapter by contract, which is what makes this legal rather than a hack, and the walk costs no extra metadata reads — every folder is discovered as a child of one already being read. An unscoped connection needs none of it: trashed = false with no parent clause enumerates the whole account in one flat, natively-paginated feed.
listMode: 'children' turns the walk off for apps that are browsing rather than importing.
Parameters
session
options
Returns
Promise<DrivePage<DriveItem>>
Implementation of
startDelta()
> startDelta(session, _options): Promise<string>
Defined in: drives-google/src/index.ts:624
The start token — "everything from now on".
folderId is accepted and ignored, because Google's change feed has no folder scope: there is one feed per account (or per shared drive). The filtering a rootId connection needs therefore happens in delta, client-side, and the README says what that costs.
Parameters
session
_options
folderId?
string
Returns
Promise<string>
Implementation of
unwatch()
> unwatch(session, watch): Promise<void>
Defined in: drives-google/src/index.ts:747
Cancels a subscription.
Parameters
session
watch
Returns
Promise<void>
Implementation of
upload()
> upload(session, input): Promise<DriveItem>
Defined in: drives-google/src/index.ts:578
Single-request multipart upload.
The metadata part and the bytes are streamed in one body rather than assembled in memory: a multipart upload that buffered its payload would hold the whole file per concurrent job, which is how an import pipeline takes a process down. The cap is enforced on the bytes that actually flow, so a source that lies about its size is caught mid-stream.
Anything over 5 MB is refused up front with DRIVE_CONTENT_TOO_LARGE: uploadType=resumable is a documented limitation, not a silent one.
Parameters
session
input
Returns
Promise<DriveItem>
Implementation of
verifyNotification()
> verifyNotification(input): DriveNotificationResult
Defined in: drives-google/src/index.ts:777
Verifies an inbound notification.
Google sends no body worth reading and no signature at all: a notification is a set of X-Goog-* headers, and the X-Goog-Channel-Token we chose at subscribe time is the whole of the authentication. So this method is a constant-time comparison's worth of work, performed by the engine against the connection's stored secret — the adapter only extracts and bounds the values.
The sync state is the "channel established" message Google sends immediately after changes.watch. It is authentic and means nothing has changed yet, so it comes back changed: false and the engine answers it with the same 200 as everything else.
Parameters
input
Returns
Implementation of
DriveProvider.verifyNotification
watch()
> watch(session, input): Promise<DriveWatch>
Defined in: drives-google/src/index.ts:715
Registers a changes.watch channel.
The channel id is ours (a UUID) and so is the token: the engine generates one random secret per subscription and Google echoes it back in X-Goog-Channel-Token, which is the only thing authenticating an inbound notification — Google does not sign them.
resourceId comes back from Google and is the other half of what channels.stop needs, so it is persisted in DriveWatch.raw. resourceUri is deliberately not persisted: it embeds the page token, and a URL that grants access to a feed does not belong in a row that gets read for display.
Parameters
session
input
Returns
Promise<DriveWatch>