Files
accounted/extensions/general/cloud-backup/lib/cloud-provider.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

128 lines
4.4 KiB
TypeScript

/**
* Provider-agnostic contract for a cloud backup target.
*
* `performSync()` builds the same archive set regardless of destination and
* talks to storage only through this interface. Everything provider-specific
* (OAuth dialect, folder model, upload protocol, checksum verification) lives
* behind an implementation: `google-provider.ts`, `dropbox-provider.ts`.
*
* Adding a provider therefore means writing one implementation and listing it
* in `provider-registry.ts`, without touching the sync engine.
*/
import type { CloudConnection, CloudProviderId } from '../types'
/**
* Thrown when a provider's token endpoint rejects a refresh attempt. Carries
* the HTTP status and raw response body so callers can distinguish a
* permanently dead refresh token (400 invalid_grant) from transient failures.
*/
export class CloudTokenRefreshError extends Error {
readonly status: number
readonly body: string
constructor(providerLabel: string, status: number, body: string) {
super(`${providerLabel} token refresh failed: ${status} ${body}`)
this.name = 'CloudTokenRefreshError'
this.status = status
this.body = body
}
/**
* True when the provider reports the refresh token itself is dead (revoked,
* expired, or the grant was invalidated). Retrying will never succeed;
* the user must re-consent. Google and Dropbox both use OAuth 2.0's
* `invalid_grant` for this.
*/
get isInvalidGrant(): boolean {
return this.status === 400 && this.body.includes('invalid_grant')
}
}
/** The three `extension_data` keys a provider owns for a company. */
export interface CloudStorageKeys {
connection: string
lastSync: string
schedule: string
}
/** Where one company's archives are written, in provider-native terms. */
export interface CloudTarget {
/** Drive: the company folder id. Dropbox: the company folder path. */
folderId: string
/** Link a human can open to reach the backup folder. */
webViewLink: string
}
export interface PreparedTarget {
target: CloudTarget
/**
* Fields to merge into the stored connection because preparing the target
* changed them (e.g. Drive re-created a folder the user trashed). Null when
* nothing changed, so the common path writes no connection record.
*/
connectionPatch: Partial<CloudConnection> | null
}
export interface PutFileResult {
/** Provider-native handle: a Drive file id, or the Dropbox path. */
id: string
name: string
size_bytes: number
}
export interface PutFileParams {
accessToken: string
target: CloudTarget
name: string
/**
* Handle recorded by the previous sync for this same file name. Providers
* that address files by id use it to update in place (and to detect that
* the user deleted the file); path-addressed providers ignore it.
*/
previousId?: string
data: ArrayBuffer
contentType: string
}
export interface PrepareTargetParams {
accessToken: string
connection: CloudConnection
/** `Testbolag AB (556000-0000)`: the per-company folder name. */
companyLabel: string
}
export interface CloudStorageProvider {
readonly id: CloudProviderId
/** Human label used in logs, errors and alert emails. */
readonly label: string
readonly keys: CloudStorageKeys
/**
* OAuth callback path relative to the extension's route root. Each provider
* needs its own: the redirect URI is registered with the provider, so they
* can never be merged after the fact.
*/
readonly callbackPath: string
/** False when this deployment has no OAuth credentials for the provider. */
isConfigured(): boolean
buildAuthorizationUrl(origin: string, state: string): string
exchangeCode(
origin: string,
code: string
): Promise<{ refreshToken: string; accountLabel: string }>
/**
* Best-effort: a failed revoke must not block a local disconnect.
* `origin` resolves the same deployment credentials as the connect flow.
*/
revoke(refreshToken: string, origin: string): Promise<void>
/** Throws {@link CloudTokenRefreshError} on rejection. */
refreshAccessToken(refreshToken: string, origin: string): Promise<string>
prepareTarget(params: PrepareTargetParams): Promise<PreparedTarget>
/**
* Write one file into the target, replacing any existing file of that name.
* Implementations verify the stored bytes against the provider's own
* checksum: a silently corrupted backup is worse than a failed one.
*/
putFile(params: PutFileParams): Promise<PutFileResult>
}