Skip to content

SourceContext

The single argument to syncExtensionContext plus what a scheduled, resumable, fan-out-capable source needs on top of it.

Type ParameterDefault type
CRecord<string, unknown>
PropertyModifierTypeDescriptionInherited from
browser?readonlyunknownA Playwright browser context, for a source whose manifest sets needsBrowser: true. unknown on purpose: typing it would put Playwright in this package’s dependency graph for the benefit of the one source that uses it. A source that needs it narrows at its own boundary. Only the Mac runtime supplies one. A needsBrowser source therefore declares runsIn: 'mac', and finding this undefined in the cloud is a misconfiguration rather than a condition to work around.-
cache?publicExtensionCacheA best-effort key/value cache that outlives a single run. Optional, and absent is normal — a host may provide no cache at all, so read it as ctx.cache?.get(...) and treat a miss and an absent cache the same way. Never the source of truth: anything here must be re-derivable from the upstream, because it can vanish between runs.ExtensionContext.cache
configreadonlyCThe user’s preference values, keyed by the field names declared in manifest.json config. Preferences only — never credentials. Feed URLs, usernames, section lists, and filters live here; auth material lives in the macOS Keychain, surfaced (PROPOSED) via ctx.credentials, never here.-
cursor?readonlyCursorThe feed’s current cursor — the position from the previous run — and absent on the first sync. Read-only: advance the cursor by returning a new Cursor from sync, not by mutating this. It was declared required here, documented as { type: 'none' } on a first run, and delivered as neither: Trove’s runtime passes undefined, the invoke contract’s own case says “a wire null cursor becomes undefined”, and every adapter tests if (!ctx.cursor). An author who believed the type and wrote ctx.cursor.type compiled cleanly and crashed on the first sync of every feed. { type: 'none' } is what a source RETURNS to mean “no new position”; it is not what it is handed. Whatever was stored comes back UNCHANGED, including a shape this union does not name — Trove keeps a cursor as opaque JSON. A source with its own checkpoint reads it as unknown and narrows at its own boundary.-
deadlinereadonlynumberWhen this round must be finished, as epoch milliseconds. A sync is given a budget, not unlimited time — in the cloud because the hosted runtime has a CPU ceiling, on the Mac because a user is waiting. A source that may run long should check this between units of work and return what it has, advancing the cursor honestly, rather than being cut off mid-item. Infinity where the host imposes no deadline.-
fetchpublicFetchLikeBehaves like the standard fetch, and is the ONLY way out. In Trove’s cloud every request is routed through an egress worker that permits https alone, matches the manifest’s declared hosts exactly, and refuses private and link-local addresses even when allowlisted. On the Mac it adds per-source timeouts, retry and rate-limit handling. Prefer it over global fetch everywhere: the global one is unguarded where it exists at all, and absent where it does not.ExtensionContext.fetch
logpublicLogChannelWhere an extension says what it is doing. Surfaced in the run transcript.ExtensionContext.log
now(): Date;

The current wall-clock time.

Injected rather than read from a global so a run is deterministic under test, and so a replayed fixture means the same thing tomorrow.

Date

ExtensionContext.now


progress(done: number, message?: string): void;

Report progress through a long run: how many items are done, and optionally a line for a person watching.

A no-op where there is no channel back — a deployed source is one request and one response, with nowhere to send an update mid-flight. Call it anyway; a source should not have to know which host it is on.

ParameterType
donenumber
message?string

void


requireSecret(name: string): Promise<string>;

secret, for a credential the extension genuinely cannot proceed without: rejects, naming the credential, rather than resolving undefined.

Prefer it to if (!(await ctx.secret(n))) throw — the failure message is the useful half, and the host writes a better one than each extension will.

ParameterType
namestring

Promise<string>

ExtensionContext.requireSecret


secret(name: string): Promise<string | undefined>;

A credential the manifest declared, by name — undefined when it is not set.

Resolves whether the value was pasted by the user or is a token Trove refreshed a moment ago — the extension cannot tell, and must not need to. That is what keeps delegated authorization out of every author’s code. Async for the same reason: resolving may be a vault read or a refresh.

This is the reader for a credential the extension can work without — an optional API key that raises a rate limit, an OAuth client secret a public client does not have. When the extension cannot proceed without the value, use requireSecret and let it raise.

Until 3.3.0 this rejected, and requireSecret was documented as behaving “identically; the name is the documentation” — so there were two names for one behaviour and no way at all to read an optional credential. The two sources that had one reached around the whole mechanism into the legacy ctx.credentials bag instead.

ParameterType
namestring

Promise<string | undefined>

ExtensionContext.secret