From f38186ab9217f874c5db82bc6dc8c0554d22d003 Mon Sep 17 00:00:00 2001 From: Andrej Guran Date: Tue, 25 Aug 2026 22:07:10 +0000 Subject: [PATCH] feat: support Google reporting SDK 2.2 --- CHANGELOG.md | 9 ++++ README.md | 6 +-- package-lock.json | 4 +- package.json | 2 +- src/bundle.test.ts | 54 +++++++++++++++++++- src/index.ts | 2 +- vendor/browser-client/package.json | 2 +- vendor/browser-client/src/index.test.ts | 4 +- vendor/browser-client/src/index.ts | 2 +- vendor/contracts/src/manifest.test.ts | 66 ++++++++++++++++++++++++- vendor/contracts/src/manifest.ts | 45 +++++++++++++++-- 11 files changed, 177 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 871f0c4..b27d482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 3.4.0 + +- Vendor the SDK 2.2.0 manifest and browser contracts so bundles can declare + app-owned Google Analytics, Search Console, and Google Ads reporting access. +- Validate provider-scoped read capabilities and require SDK 2.2.0 for those + reporting integrations while retaining SDK 2.0.0 and 2.1.0 compatibility. +- Default newly bundled apps to SDK 2.2.0 and preserve reporting declarations + in deterministic archives for development and deployment. + ## 3.3.1 - Accept protected schema-2 agent runtime sessions before a new app receives diff --git a/README.md b/README.md index df83cde..0ca5cac 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,12 @@ offline source bundle, but cannot connect to or deploy through OpenCloud. ## Install a pinned release -OpenCloud application skills pin an exact CLI release. To install `v3.3.1` in +OpenCloud application skills pin an exact CLI release. To install `v3.4.0` in an isolated task directory: ```bash -OPENCLOUD_CLI_VERSION="v3.3.1" -OPENCLOUD_CLI_PACKAGE="opencloud-cli-3.3.1.tgz" +OPENCLOUD_CLI_VERSION="v3.4.0" +OPENCLOUD_CLI_PACKAGE="opencloud-cli-3.4.0.tgz" OPENCLOUD_CLI_DIR="$(mktemp -d)" curl -fsSLo "$OPENCLOUD_CLI_DIR/$OPENCLOUD_CLI_PACKAGE" \ diff --git a/package-lock.json b/package-lock.json index be8df7a..d76aaff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@opencloud/cli", - "version": "3.3.1", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@opencloud/cli", - "version": "3.3.1", + "version": "3.4.0", "dependencies": { "@napi-rs/keyring": "1.3.0" }, diff --git a/package.json b/package.json index b7ac166..dd9cdc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@opencloud/cli", - "version": "3.3.1", + "version": "3.4.0", "description": "Versioned command-line client for building, deploying, and verifying OpenCloud applications", "type": "module", "bin": { diff --git a/src/bundle.test.ts b/src/bundle.test.ts index a32fba5..ac813c7 100644 --- a/src/bundle.test.ts +++ b/src/bundle.test.ts @@ -115,7 +115,7 @@ functions: expect(first.manifest.migrations[0]?.sha256).toMatch(/^[a-f0-9]{64}$/); expect(first.manifest.runtime).toEqual({ sdk: { - version: "2.1.0", + version: "2.2.0", }, }); expect(first.files).toEqual([ @@ -268,6 +268,58 @@ integrations: expect(archived.integrations).toEqual(bundle.manifest.integrations); }); + it("validates and archives Google reporting integrations with SDK 2.2", async () => { + const root = await temporaryDirectory(); + await mkdir(path.join(root, "frontend")); + await writeFile(path.join(root, "frontend", "index.html"), "hello"); + await writeManifest( + root, + ` +frontend: + directory: frontend +runtime: + sdk: + version: 2.2.0 +integrations: + analytics: + provider: google-analytics + account: app + capabilities: + - analytics.reports.read + search: + provider: google-search-console + account: app + capabilities: + - search.performance.read + ads: + provider: google-ads + account: app + capabilities: + - ads.reporting.read +`, + ); + + const bundle = await buildBundle(root); + const archived = await readArchivedManifest(root, bundle.archive); + + expect(bundle.manifest.runtime.sdk.version).toBe("2.2.0"); + expect(bundle.manifest.integrations).toMatchObject({ + analytics: { + provider: "google-analytics", + capabilities: ["analytics.reports.read"], + }, + search: { + provider: "google-search-console", + capabilities: ["search.performance.read"], + }, + ads: { + provider: "google-ads", + capabilities: ["ads.reporting.read"], + }, + }); + expect(archived.integrations).toEqual(bundle.manifest.integrations); + }); + it("never archives local .opencloud development metadata", async () => { const root = await temporaryDirectory(); await mkdir(path.join(root, ".opencloud")); diff --git a/src/index.ts b/src/index.ts index 0840386..3701100 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,7 @@ import { resolveWorkspaceFile, } from "./workspace-store.js"; -const CLI_VERSION = "3.3.1"; +const CLI_VERSION = "3.4.0"; const program = new Command() .name("opencloud") diff --git a/vendor/browser-client/package.json b/vendor/browser-client/package.json index 6aa387f..d61a95b 100644 --- a/vendor/browser-client/package.json +++ b/vendor/browser-client/package.json @@ -1,6 +1,6 @@ { "name": "@opencloud/js", - "version": "2.1.0", + "version": "2.2.0", "private": true, "type": "module" } diff --git a/vendor/browser-client/src/index.test.ts b/vendor/browser-client/src/index.test.ts index 5993fd7..88c18ac 100644 --- a/vendor/browser-client/src/index.test.ts +++ b/vendor/browser-client/src/index.test.ts @@ -33,7 +33,7 @@ const runtimeConfig = { environment: "production", sdk: { package: "@opencloud/js", - version: "2.1.0", + version: "2.2.0", module: "/_opencloud/sdk.js", types: "/_opencloud/sdk.d.ts", docs: "https://docs.opencloud.ai/sdk/javascript/", @@ -184,7 +184,7 @@ afterEach(() => { describe("@opencloud/js v2", () => { it("exports one stable singleton contract without legacy factories or raw namespaces", () => { - expect(OPEN_CLOUD_SDK_VERSION).toBe("2.1.0"); + expect(OPEN_CLOUD_SDK_VERSION).toBe("2.2.0"); expect("OPEN_CLOUD_JS_VERSION" in sdk).toBe(false); expect(opencloud).toMatchObject({ app: { info: expect.any(Function) }, diff --git a/vendor/browser-client/src/index.ts b/vendor/browser-client/src/index.ts index 2daf6e1..478d817 100644 --- a/vendor/browser-client/src/index.ts +++ b/vendor/browser-client/src/index.ts @@ -6,7 +6,7 @@ * HTTP responses deliberately stay behind this module. */ -export const OPEN_CLOUD_SDK_VERSION = "2.1.0"; +export const OPEN_CLOUD_SDK_VERSION = "2.2.0"; export type OpenCloudEnvironment = "dev" | "production"; export type OpenCloudVisibility = "public" | "private"; diff --git a/vendor/contracts/src/manifest.test.ts b/vendor/contracts/src/manifest.test.ts index 04fbdd6..f387105 100644 --- a/vendor/contracts/src/manifest.test.ts +++ b/vendor/contracts/src/manifest.test.ts @@ -47,7 +47,7 @@ describe("OpenCloud manifest", () => { }); it("requires an exact installed deployment-pinned SDK version", () => { - for (const version of ["2.0.0", "2.1.0"]) { + for (const version of ["2.0.0", "2.1.0", "2.2.0"]) { expect( parseManifest({ ...valid, @@ -64,7 +64,7 @@ describe("OpenCloud manifest", () => { ...valid, runtime: { sdk: { version } }, }), - ).toThrow(/installed SDK version: 2\.0\.0 or 2\.1\.0/); + ).toThrow(/installed SDK version: 2\.0\.0, 2\.1\.0, or 2\.2\.0/); } }); @@ -515,6 +515,68 @@ describe("OpenCloud manifest", () => { ).toThrow(/not supported by google-drive/); }); + it("declares app-owned Google reporting resources", () => { + const integrations = parseManifest({ + ...valid, + runtime: { sdk: { version: "2.2.0" } }, + integrations: { + analytics: { + provider: "google-analytics", + account: "app", + capabilities: ["analytics.reports.read"], + }, + search: { + provider: "google-search-console", + account: "app", + cardinality: "many", + capabilities: ["search.performance.read"], + }, + ads: { + provider: "google-ads", + account: "app", + capabilities: ["ads.reporting.read"], + }, + }, + }).integrations; + + expect(integrations).toMatchObject({ + analytics: { provider: "google-analytics", cardinality: "one" }, + search: { provider: "google-search-console", cardinality: "many" }, + ads: { provider: "google-ads", cardinality: "one" }, + }); + for (const [provider, capability] of [ + ["google-analytics", "analytics.reports.read"], + ["google-search-console", "search.performance.read"], + ["google-ads", "ads.reporting.read"], + ] as const) { + expect(() => + parseManifest({ + ...valid, + runtime: { sdk: { version: "2.2.0" } }, + integrations: { + reporting: { + provider, + account: "calling_user", + capabilities: [capability], + }, + }, + }), + ).toThrow(new RegExp(`${provider} integrations must use the app account`)); + } + expect(() => + parseManifest({ + ...valid, + integrations: { + analytics: { + provider: "google-analytics", + account: "app", + capabilities: ["analytics.reports.read"], + }, + }, + }), + ).toThrow(/google-analytics requires runtime SDK version 2\.2\.0 or later/); + }); + it("declares app-owned HubSpot CRM access and bounds associations", () => { expect( parseManifest({ diff --git a/vendor/contracts/src/manifest.ts b/vendor/contracts/src/manifest.ts index c50f959..464acdf 100644 --- a/vendor/contracts/src/manifest.ts +++ b/vendor/contracts/src/manifest.ts @@ -30,8 +30,8 @@ const sameOriginAbsolutePath = z const digest = z.string().regex(/^[a-f0-9]{64}$/, "expected a SHA-256 digest"); /** Exact immutable SDK artifacts installed by this platform release. */ -export const sdkVersionSchema = z.enum(["2.0.0", "2.1.0"], { - error: "expected an installed SDK version: 2.0.0 or 2.1.0", +export const sdkVersionSchema = z.enum(["2.0.0", "2.1.0", "2.2.0"], { + error: "expected an installed SDK version: 2.0.0, 2.1.0, or 2.2.0", }); export const migrationSchema = z @@ -97,6 +97,9 @@ export const integrationCapabilitySchema = z.enum([ "docs.documents.write", "slides.presentations.read", "slides.presentations.write", + "analytics.reports.read", + "search.performance.read", + "ads.reporting.read", "bank.accounts.read", "bank.balances.read", "bank.transactions.read", @@ -138,6 +141,9 @@ export const integrationProviderSchema = z.enum([ "google-sheets", "google-docs", "google-slides", + "google-analytics", + "google-search-console", + "google-ads", "gocardless-bank-account-data", "wise-balance-webhook", "slack", @@ -164,6 +170,9 @@ const capabilitiesForProvider: Record< "slides.presentations.read", "slides.presentations.write", ]), + "google-analytics": new Set(["analytics.reports.read"]), + "google-search-console": new Set(["search.performance.read"]), + "google-ads": new Set(["ads.reporting.read"]), "gocardless-bank-account-data": new Set([ "bank.accounts.read", "bank.balances.read", @@ -258,7 +267,16 @@ export const integrationDefinitionSchema = z message: "wise-balance-webhook integrations must use the app account", }); } - if (["slack", "telegram", "hubspot-crm"].includes(definition.provider)) { + if ( + [ + "slack", + "telegram", + "hubspot-crm", + "google-analytics", + "google-search-console", + "google-ads", + ].includes(definition.provider) + ) { if (definition.account !== "app") { context.addIssue({ code: "custom", @@ -553,14 +571,31 @@ export const openCloudManifestSchema = z .superRefine((manifest, context) => { if ( manifest.notifications?.webPush && - manifest.runtime.sdk.version !== "2.1.0" + manifest.runtime.sdk.version === "2.0.0" ) { context.addIssue({ code: "custom", path: ["notifications", "webPush"], - message: "Web Push notifications require runtime SDK version 2.1.0", + message: "Web Push notifications require runtime SDK version 2.1.0 or later", }); } + for (const [name, integration] of Object.entries(manifest.integrations)) { + if ( + [ + "google-analytics", + "google-search-console", + "google-ads", + ].includes(integration.provider) && + (manifest.runtime.sdk.version === "2.0.0" || + manifest.runtime.sdk.version === "2.1.0") + ) { + context.addIssue({ + code: "custom", + path: ["integrations", name, "provider"], + message: `${integration.provider} requires runtime SDK version 2.2.0 or later`, + }); + } + } const assertUnique = ( values: string[], path: