From 17c06225a6cfcda599b2a5219cb1ce912f166a4c Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:19:49 -0400 Subject: [PATCH 01/20] fix(frontend): validate the generated plugin allowlist at load, confirm every non-GET playground row, and probe the capability handshake under the mount prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Items 1-4 of #1121, from the independent review of #1116. N1/N2 — the composition's whole prefix rule, re-checked where this host reads the generated allowlist. `isFrontendServiceRoutePrefix` and `isServiceableFrontendService` are new in the contract package, and the composition validator, the load-time resolution and the BFF handler all read them rather than each restating part of the rule. Comparing against the origin root alone admitted `""`, which is not the root: `frontendServiceTargetPath("", path)` lands every relative path on the upstream ORIGIN, so a stale or hand-edited `rest` entry proxied its whole backend under the viewer's forwarded bearer (the review's probe, `admin/secret`, answered 200). A `connectServices` that was not an array has a `length` and no `every`, so it threw out of the handler as a 500 instead of refusing the alias as unserviceable. N3 — every non-GET playground row is armed before it sends. The arming gate read `transportCarries`, which answers for the DEMO declaration while the request goes to the plugin the operator named: an alias the demo declares `connect` may be `rest` there, so a row the table shows as refused is one that backend carries — and it left the browser on the first press. N4 — the Connect capability probe lands under the requirement's mount prefix, like every other procedure of that alias. It was sent to the origin root whatever the prefix said, so a backend mounting Connect under `/v1/connect` answered the probe with its own 404 and the mounted form this host advertises could never complete a handshake. N6/m3/N5 — the architecture doc no longer claims "neither side can admit a shape the other refuses": the host is the authority and the client is agreement, and a runtime created without the installed requirements applies the generic path rule alone. The raw-path test now says what it proves and what it assumes about the framework's decoding. The contract package's README records the 2.4.4 patch-versus-breaking call and the three facts that decided it. saas-plugin-contract 2.5.0: additive exports, so a minor, and `^2` consumers take it unchanged. saas-sdk 0.3.30 carries the regenerated bindings. Co-Authored-By: Claude Opus 5 (1M context) --- module/docs/frontend-plugin-architecture.md | 13 +- module/docs/frontend-plugin-bff-contract.md | 38 ++++-- .../code/frontend-plugin-public-api.json | 4 +- .../services/frontend/code/package-lock.json | 14 +-- module/services/frontend/code/package.json | 4 +- .../code/packages/codefly-ui/package.json | 2 +- .../packages/saas-plugin-contract/README.md | 30 +++++ .../saas-plugin-contract/package.json | 2 +- .../saas-plugin-contract/src/composition.ts | 28 ++--- .../saas-plugin-contract/src/index.ts | 2 + .../saas-plugin-contract/src/transport.ts | 75 +++++++++++ .../test/transport.test.ts | 117 ++++++++++++++++++ .../saas-plugin-manifest/package.json | 2 +- .../packages/saas-plugin-react/package.json | 2 +- .../frontend/code/server/plugin-bff.ts | 62 ++++++---- .../code/server/plugin-service-bindings.ts | 27 +++- .../__tests__/route.raw-path.test.ts | 11 ++ .../model/demo-plugin.test.ts | 26 ++-- .../plugin-transport/model/demo-plugin.ts | 23 +++- ...in-transport-playground.container.test.tsx | 23 ++++ .../ui/plugin-transport-playground.tsx | 16 ++- .../code/src/lib/__tests__/plugin-bff.test.ts | 90 +++++++++++++- .../__tests__/plugin-service-bindings.test.ts | 63 ++++++++++ 23 files changed, 579 insertions(+), 95 deletions(-) diff --git a/module/docs/frontend-plugin-architecture.md b/module/docs/frontend-plugin-architecture.md index 4e6218c4c..67f16b295 100644 --- a/module/docs/frontend-plugin-architecture.md +++ b/module/docs/frontend-plugin-architecture.md @@ -87,8 +87,17 @@ silently falls back to sample data. package and read by both the browser transport and the BFF: safe segments, with a Google custom verb allowed on the last one and its colon forwarded literal, and a Connect procedure of a service the requirement declared. Both - REST and Connect bindings are therefore reachable from a contributed page, and - neither side can admit a shape the other refuses. + REST and Connect bindings are therefore reachable from a contributed page. + **The host is the authority and the client is agreement, so the two are not + symmetric.** The browser applies the Connect rule only when the runtime was + given the installed requirements (`createPluginRuntime({ services })`); + without them it applies the generic path rule alone, because a client that + guessed would refuse routes a composition had legitimately installed. It also + never applies the serviceability rules below, which are about the resolved + allowlist entry and not about the path. So the client can admit a shape the + BFF refuses, which is the direction that is safe; the reverse — the BFF + forwarding something the client would have refused — cannot happen, because + the one path rule is the host's and the host runs it on every request. - A `connect` requirement names the services it may reach, and the list is required. The protocol puts the service name in the path, so the caller supplies it and the route prefix cannot bound it: the prefix says where the diff --git a/module/docs/frontend-plugin-bff-contract.md b/module/docs/frontend-plugin-bff-contract.md index 7befe599d..c7422ccca 100644 --- a/module/docs/frontend-plugin-bff-contract.md +++ b/module/docs/frontend-plugin-bff-contract.md @@ -28,12 +28,23 @@ host, port, module, service, endpoint, or upstream prefix from a browser. `..:recover` is a traversal wearing a verb and would bind `..` as the resource upstream. 5. The resolved allowlist entry is held to the composition's own rules a second - time, at runtime. The BFF reads the GENERATED allowlist rather than the - composition that validated it, so a build-time rule nothing re-checks is one - a stale or hand-edited projection walks straight past: a `rest` entry - declaring the origin root, and a `connect` entry declaring no services it may - reach, are both `503 backend_unavailable` — the alias cannot be served, and - no request the caller could phrase differently would change that. + time — at LOAD, where the generated allowlist is read, and again in the + handler. The BFF reads the GENERATED allowlist rather than the composition + that validated it, so a build-time rule nothing re-checks is one a stale or + hand-edited projection walks straight past, and the rule is the whole one + rather than part of it (`isServiceableFrontendService`): the prefix must be + an absolute path of safe, non-dot segments, with the origin root admitted + only on a `connect` entry; a `connect` entry must declare a non-empty list + of fully qualified services, as a list. Anything else is + `503 backend_unavailable` — the alias cannot be served, and no request the + caller could phrase differently would change that. + + Checking only against the root was not enough, and `""` is why: it is not + the root, so it passed, and every relative path then landed on the upstream + ORIGIN — `admin/secret` reached `https://upstream/admin/secret` and was + answered under the viewer's own forwarded bearer. A `connectServices` that + was not a list has a `length` and no `every`, which threw out of the handler + as a `500` instead of refusing the alias. ## Which bindings the transport carries @@ -57,11 +68,18 @@ two things: where its upstream mounts Connect, and which of that upstream's services the alias may reach. - `routePrefix` is the mount. `/` is the origin root, where the Connect protocol - puts a unary procedure and where this contract's own fixed capability - handshake has always been probed; a product path prefix such as `/v1/connect` - is a Connect mount behind a path, and the procedure lands under it. The root - is Connect-only: a REST requirement declaring it would turn its alias into a + puts a unary procedure; a product path prefix such as `/v1/connect` is a + Connect mount behind a path, and the procedure lands under it. The root is + Connect-only: a REST requirement declaring it would turn its alias into a prefix-less proxy of the whole upstream. + + **The capability handshake is a procedure of the alias like any other, so it + is probed under the mount too.** It used to be sent to the origin root + whatever the prefix said, which is right only for a root-prefixed + requirement: a backend mounting Connect under `/v1/connect` answered the + probe with its own 404, the alias was reported incompatible, and the mounted + form this host advertises could never complete a handshake. (A REST alias's + probe path is declared absolute and is not relative to the prefix.) - `connectServices` is the bound, and it is **required** and non-empty. A Connect procedure's path is `//`, so the CALLER supplies the service name — which means the prefix cannot say which diff --git a/module/services/frontend/code/frontend-plugin-public-api.json b/module/services/frontend/code/frontend-plugin-public-api.json index ca4dedf90..8190a7aab 100644 --- a/module/services/frontend/code/frontend-plugin-public-api.json +++ b/module/services/frontend/code/frontend-plugin-public-api.json @@ -4,7 +4,7 @@ "packages": { "@codefly-dev/saas-plugin-contract": { "status": "active", - "packageVersion": "2.4.4", + "packageVersion": "2.5.0", "entrypoints": [".", "./capabilities"], "entrypointExports": { ".": [ @@ -93,6 +93,8 @@ "isConnectProcedurePath", "isConnectServiceName", "isFrontendServicePath", + "isFrontendServiceRoutePrefix", + "isServiceableFrontendService", "resolveConnectProcedure", "resolveFrontendAppearance", "resolveSkinRules", diff --git a/module/services/frontend/code/package-lock.json b/module/services/frontend/code/package-lock.json index 7e6086c73..f83db0cf8 100644 --- a/module/services/frontend/code/package-lock.json +++ b/module/services/frontend/code/package-lock.json @@ -13,9 +13,9 @@ "dependencies": { "@base-ui/react": "^1.3.0", "@bufbuild/protobuf": "^2.11.0", - "@codefly-dev/saas-plugin-contract": "2.4.4", + "@codefly-dev/saas-plugin-contract": "2.5.0", "@codefly-dev/saas-plugin-react": "0.4.5", - "@codefly-dev/saas-sdk": "0.3.29", + "@codefly-dev/saas-sdk": "0.3.30", "@codefly-dev/saas-ui": "0.12.8", "@codefly-dev/ui": "0.12.8", "@codefly/saas-plugin-manifest": "0.1.2", @@ -15382,7 +15382,7 @@ "tailwind-merge": "^3.5.0" }, "devDependencies": { - "@codefly-dev/saas-plugin-contract": "2.4.4", + "@codefly-dev/saas-plugin-contract": "2.5.0", "@codefly-dev/saas-plugin-react": "0.4.5", "@tailwindcss/postcss": "^4", "@testing-library/react": "^16.1.0", @@ -15429,7 +15429,7 @@ }, "packages/saas-plugin-contract": { "name": "@codefly-dev/saas-plugin-contract", - "version": "2.4.4", + "version": "2.5.0", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.11.0" @@ -15444,7 +15444,7 @@ "version": "0.1.2", "license": "Apache-2.0", "dependencies": { - "@codefly-dev/saas-plugin-contract": "2.4.4" + "@codefly-dev/saas-plugin-contract": "2.5.0" }, "devDependencies": { "@types/js-yaml": "^4.0.9", @@ -15459,7 +15459,7 @@ "version": "0.4.5", "license": "Apache-2.0", "devDependencies": { - "@codefly-dev/saas-plugin-contract": "2.4.4", + "@codefly-dev/saas-plugin-contract": "2.5.0", "@testing-library/react": "^16.1.0", "@types/react": "19.2.17", "happy-dom": "^20.8.9", @@ -15474,7 +15474,7 @@ }, "packages/saas-sdk": { "name": "@codefly-dev/saas-sdk", - "version": "0.3.29", + "version": "0.3.30", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.11.0", diff --git a/module/services/frontend/code/package.json b/module/services/frontend/code/package.json index e363b7c54..a780ed293 100644 --- a/module/services/frontend/code/package.json +++ b/module/services/frontend/code/package.json @@ -37,9 +37,9 @@ "dependencies": { "@base-ui/react": "^1.3.0", "@bufbuild/protobuf": "^2.11.0", - "@codefly-dev/saas-plugin-contract": "2.4.4", + "@codefly-dev/saas-plugin-contract": "2.5.0", "@codefly-dev/saas-plugin-react": "0.4.5", - "@codefly-dev/saas-sdk": "0.3.29", + "@codefly-dev/saas-sdk": "0.3.30", "@codefly-dev/saas-ui": "0.12.8", "@codefly-dev/ui": "0.12.8", "@codefly/saas-plugin-manifest": "0.1.2", diff --git a/module/services/frontend/code/packages/codefly-ui/package.json b/module/services/frontend/code/packages/codefly-ui/package.json index 1c7bbdf5a..7f9be3b10 100644 --- a/module/services/frontend/code/packages/codefly-ui/package.json +++ b/module/services/frontend/code/packages/codefly-ui/package.json @@ -116,7 +116,7 @@ "react": ">=19.2 <20" }, "devDependencies": { - "@codefly-dev/saas-plugin-contract": "2.4.4", + "@codefly-dev/saas-plugin-contract": "2.5.0", "@codefly-dev/saas-plugin-react": "0.4.5", "@testing-library/react": "^16.1.0", "@tailwindcss/postcss": "^4", diff --git a/module/services/frontend/code/packages/saas-plugin-contract/README.md b/module/services/frontend/code/packages/saas-plugin-contract/README.md index 58a6b038c..bea8f000b 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/README.md +++ b/module/services/frontend/code/packages/saas-plugin-contract/README.md @@ -219,3 +219,33 @@ codefly generate proto --proto ./proto --output . --local --template buf.gen.loc The package root and `./capabilities` are public. The default light muted foreground uses achromatic OKLCH lightness 0.54 so small captions meet WCAG AA 4.5:1 on background, card, muted and accent surfaces. The default dark pair also satisfies this check. Custom appearances own their chosen contrast; clients should use these semantic tokens rather than inventing local caption colors. + +## Version history + +There is no separate changelog: this package's released shape is small enough +that the versions worth knowing about are the ones that changed what a +composition may declare, and that record belongs beside the rules. + +- **2.5.0** — `isFrontendServiceRoutePrefix` and `isServiceableFrontendService` + are exported: the whole route-prefix rule, and what a host may proxy at all, + as predicates both the composition and the host's runtime read. A MINOR, not + a patch, because the public surface grew; `^2` consumers take it unchanged. + The composition refuses nothing it admitted in 2.4.4 — the host's own + re-check gained the parts of the rule it was missing, which is where a stale + or hand-edited allowlist used to walk past a prefix the composition would + never have validated. +- **2.4.4** — `connectServices` became required and non-empty on a `connect` + requirement, and rejected on a `rest` one. **Released as a PATCH, and that + was a deliberate call rather than an oversight.** Semver would normally read + a validation that refuses a previously accepted declaration as breaking: a + `connect` requirement without the new field fails composition. Three facts + decided it the other way. The shape it tightens shipped one day earlier in + 2.4.3 (the root Connect prefix), so the window in which a published + composition could have declared it was a day wide; the only would-be consumer + vendored 2.4.0, which has neither the field nor the prefix; and a major would + have forced every remote's peer range off `^2` — cascading a breaking minor + through the co-versioned `0.x` kit — for a capability nobody was consuming. + The alternative to the tightening was leaving a Connect alias reaching every + service its upstream origin served, as the viewer. +- **2.4.3** — a `connect` requirement may declare the origin root (`/`) as its + route prefix, which is where the Connect protocol puts a procedure. diff --git a/module/services/frontend/code/packages/saas-plugin-contract/package.json b/module/services/frontend/code/packages/saas-plugin-contract/package.json index 27422a5bc..688b091ca 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/package.json +++ b/module/services/frontend/code/packages/saas-plugin-contract/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/saas-plugin-contract", - "version": "2.4.4", + "version": "2.5.0", "description": "React-free serializable frontend plugin metadata and composition for the Codefly SaaS host.", "type": "module", "sideEffects": false, diff --git a/module/services/frontend/code/packages/saas-plugin-contract/src/composition.ts b/module/services/frontend/code/packages/saas-plugin-contract/src/composition.ts index 04a0b3925..f474b9c73 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/src/composition.ts +++ b/module/services/frontend/code/packages/saas-plugin-contract/src/composition.ts @@ -15,8 +15,8 @@ import { type PresentationRequirement, } from "./contracts.js"; import { - FRONTEND_SERVICE_ROOT_PREFIX, isConnectServiceName, + isFrontendServiceRoutePrefix, } from "./transport.js"; function assertContribution( @@ -240,26 +240,12 @@ function validateServiceRequirement( service.protocol === "connect" || service.protocol === "rest", `plugin '${pluginName}' service '${service.alias}' uses unsupported protocol '${String(service.protocol)}'`, ); - const routeSegments = - typeof service.routePrefix === "string" - ? service.routePrefix.slice(1).split("/") - : []; - // The origin root is a prefix only a Connect requirement may declare, and - // declaring it is the only way to say what the Connect protocol already - // decides: a procedure's path is `//` from - // the origin, which is where this contract's own fixed capability handshake - // has always been probed. A REST requirement declaring the root would turn - // the alias into a prefix-less proxy of its whole upstream instead. - const rootPrefix = - service.protocol === "connect" && - service.routePrefix === FRONTEND_SERVICE_ROOT_PREFIX; - assertContribution( - typeof service.routePrefix === "string" && - (rootPrefix || - (SAFE_ROUTE_PREFIX.test(service.routePrefix) && - routeSegments.every( - (segment) => segment !== "." && segment !== "..", - ))), + // The prefix rule is `isFrontendServiceRoutePrefix` and is not restated + // here: the host's BFF re-checks the same rule at load and at request time, + // and the two readings drifting apart is what let a REST entry declaring + // `""` proxy its whole upstream (issue #1121). + assertContribution( + isFrontendServiceRoutePrefix(service.protocol, service.routePrefix), `plugin '${pluginName}' service '${service.alias}' route prefix '${String(service.routePrefix)}' is unsafe`, ); // On a Connect alias the caller supplies the service name in the path, diff --git a/module/services/frontend/code/packages/saas-plugin-contract/src/index.ts b/module/services/frontend/code/packages/saas-plugin-contract/src/index.ts index 215bcb813..03d270705 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/src/index.ts +++ b/module/services/frontend/code/packages/saas-plugin-contract/src/index.ts @@ -70,6 +70,8 @@ export { isConnectProcedurePath, isConnectServiceName, isFrontendServicePath, + isFrontendServiceRoutePrefix, + isServiceableFrontendService, resolveConnectProcedure, } from "./transport.js"; export type { diff --git a/module/services/frontend/code/packages/saas-plugin-contract/src/transport.ts b/module/services/frontend/code/packages/saas-plugin-contract/src/transport.ts index dc8a6f9f8..1a856492e 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/src/transport.ts +++ b/module/services/frontend/code/packages/saas-plugin-contract/src/transport.ts @@ -8,6 +8,8 @@ * the package both already depend on, so one change moves both sides. */ +import type { FrontendServiceProtocol } from "./contracts.js"; + /** A path segment with no reserved character in it. */ const SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/; @@ -38,6 +40,79 @@ const MAX_SEGMENT_LENGTH = 256; */ export const FRONTEND_SERVICE_ROOT_PREFIX = "/"; +/** + * An absolute upstream path prefix: one or more safe segments, each with no + * reserved character in it. Deliberately narrower than a URL path — a prefix is + * where the host mounts an alias, so a query, a fragment, an empty segment or a + * trailing slash are all shapes it has no meaning for. + */ +const SAFE_ROUTE_PREFIX = /^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/; + +/** + * Whether this requirement's route prefix is one the host may mount, for the + * protocol it declares. It is the WHOLE rule, in one place, because two + * different readings of it are what the BFF walked past: the composition + * validated the full form at build time while the runtime re-check compared + * only against the root, so a prefix the composition would have refused — + * `""`, `/..`, `/a//b` — passed the runtime one and landed its relative path + * straight on the upstream origin. + * + * The origin root is Connect-only, and declaring it is the only way to say what + * the Connect protocol already decides: a procedure's path is + * `//` from the origin. A REST requirement + * declaring it would be a prefix-less proxy of its whole upstream, and so would + * any prefix that is not an absolute path of safe, non-dot segments. + */ +export function isFrontendServiceRoutePrefix( + protocol: FrontendServiceProtocol, + routePrefix: unknown, +): routePrefix is string { + if (typeof routePrefix !== "string") return false; + if (protocol === "connect" && routePrefix === FRONTEND_SERVICE_ROOT_PREFIX) { + return true; + } + return ( + SAFE_ROUTE_PREFIX.test(routePrefix) && + routePrefix + .slice(1) + .split("/") + .every((segment) => segment !== "." && segment !== "..") + ); +} + +/** + * What a requirement must look like for the host to proxy it at all: its prefix + * is one the protocol admits, and a Connect one declares a usable list of the + * services it may reach. + * + * Both halves are the composition's own rules, and this is the one function + * that states them — the BFF reads the GENERATED allowlist rather than the + * composition that validated it, so a build-time rule no runtime re-checks is + * one a stale or hand-edited projection walks straight past. `Array.isArray` is + * part of the rule and not defensive noise: a projection whose + * `connectServices` is a string has a `length` and no `every`, so a check that + * assumed an array threw out of the request handler as a 500 instead of + * refusing the alias as unserviceable. + */ +export function isServiceableFrontendService(service: { + protocol: FrontendServiceProtocol; + routePrefix: string; + connectServices?: readonly string[]; +}): boolean { + if (!isFrontendServiceRoutePrefix(service.protocol, service.routePrefix)) { + return false; + } + if (service.protocol !== "connect") return true; + // Connect without a declared service list reaches nothing: the path names + // the service, so an absent — or non-array — list is an unbounded alias. + const services = service.connectServices; + return ( + Array.isArray(services) && + services.length > 0 && + services.every(isConnectServiceName) + ); +} + function usableSegment(segment: string): boolean { return ( typeof segment === "string" && diff --git a/module/services/frontend/code/packages/saas-plugin-contract/test/transport.test.ts b/module/services/frontend/code/packages/saas-plugin-contract/test/transport.test.ts index 8dfb0ce41..13bb38dab 100644 --- a/module/services/frontend/code/packages/saas-plugin-contract/test/transport.test.ts +++ b/module/services/frontend/code/packages/saas-plugin-contract/test/transport.test.ts @@ -7,6 +7,8 @@ import { isConnectProcedurePath, isConnectServiceName, isFrontendServicePath, + isFrontendServiceRoutePrefix, + isServiceableFrontendService, resolveConnectProcedure, } from "../src/transport.js"; @@ -217,3 +219,118 @@ describe("the path a declared binding lands on", () => { expect(frontendServiceTargetPath("/", [])).toBe("/"); }); }); + +// THE WHOLE PREFIX RULE, IN ONE PLACE (issue #1121). +// +// The composition validated the full form at build time while the host's +// runtime re-check compared only against the root, so a prefix the composition +// would have refused passed the runtime one — and a REST entry declaring `""` +// landed every relative path on the upstream origin. Both sides read this now, +// so the table below is the rule both obey. +describe("isFrontendServiceRoutePrefix", () => { + it.each([ + ["an absolute path of safe segments", "rest", "/v1/example", true], + ["one segment", "rest", "/v1", true], + // The root is Connect-only: it says "where the Connect protocol puts a + // procedure", which is the origin. A REST alias declaring it has no + // prefix at all. + ["the root on a connect requirement", "connect", "/", true], + ["the root on a rest requirement", "rest", "/", false], + // The empty string is NOT the root, which is exactly why comparing + // against the root alone admitted it. + ["the empty string", "rest", "", false], + ["the empty string on connect", "connect", "", false], + ["a relative prefix", "rest", "v1/example", false], + ["a traversal", "rest", "/v1/../private", false], + ["a dot segment", "rest", "/v1/./private", false], + ["an empty segment", "rest", "/v1//private", false], + ["a trailing slash", "rest", "/v1/example/", false], + ["an encoded separator", "rest", "/v1/%2fprivate", false], + ["a query", "rest", "/v1/example?all=1", false], + ["a fragment", "rest", "/v1/example#x", false], + ])("%s", (_name, protocol, prefix, admitted) => { + expect( + isFrontendServiceRoutePrefix(protocol as "connect" | "rest", prefix), + ).toBe(admitted); + }); + + it("refuses anything that is not a string", () => { + for (const value of [undefined, null, 1, {}, ["/v1"]]) { + expect(isFrontendServiceRoutePrefix("rest", value)).toBe(false); + expect(isFrontendServiceRoutePrefix("connect", value)).toBe(false); + } + }); +}); + +// What a host may proxy at all, independent of the path asked for. +describe("isServiceableFrontendService", () => { + it("admits the two shapes a composition installs", () => { + expect( + isServiceableFrontendService({ + protocol: "rest", + routePrefix: "/v1/example", + }), + ).toBe(true); + expect( + isServiceableFrontendService({ + protocol: "connect", + routePrefix: "/", + connectServices: ["example.records.v1.RecordService"], + }), + ).toBe(true); + expect( + isServiceableFrontendService({ + protocol: "connect", + routePrefix: "/v1/connect", + connectServices: ["example.records.v1.RecordService"], + }), + ).toBe(true); + }); + + // The probe the review used: a REST entry whose prefix is the empty string + // proxies its whole upstream, because every relative path lands at the + // origin. `admin/secret` is a real path on a real backend. + it("refuses a rest entry with no prefix", () => { + expect( + isServiceableFrontendService({ protocol: "rest", routePrefix: "" }), + ).toBe(false); + expect(frontendServiceTargetPath("", ["admin", "secret"])).toBe( + "/admin/secret", + ); + }); + + // A Connect entry whose service list is not an array has a `length` and no + // `every`: a check that assumed an array threw out of the request handler + // as a 500 rather than refusing the alias. + it.each([ + ["a string", "example.records.v1.RecordService"], + ["an object", { 0: "example.records.v1.RecordService", length: 1 }], + ["a number", 1], + ["null", null], + ["absent", undefined], + ["empty", []], + ["an unusable name", ["not a service name"]], + ["a non-string entry", [1]], + ])("refuses a connect entry whose services are %s", (_name, services) => { + expect( + isServiceableFrontendService({ + protocol: "connect", + routePrefix: "/", + connectServices: services as unknown as readonly string[], + }), + ).toBe(false); + }); + + // On a REST alias the field is meaningless rather than disqualifying: the + // composition refuses it, and a projection carrying one is still bounded by + // its prefix. + it("ignores connect services on a rest entry", () => { + expect( + isServiceableFrontendService({ + protocol: "rest", + routePrefix: "/v1/example", + connectServices: ["nonsense"] as unknown as readonly string[], + }), + ).toBe(true); + }); +}); diff --git a/module/services/frontend/code/packages/saas-plugin-manifest/package.json b/module/services/frontend/code/packages/saas-plugin-manifest/package.json index f7fbba893..6c1350cb8 100644 --- a/module/services/frontend/code/packages/saas-plugin-manifest/package.json +++ b/module/services/frontend/code/packages/saas-plugin-manifest/package.json @@ -23,7 +23,7 @@ "test": "vitest run --config vitest.config.ts" }, "dependencies": { - "@codefly-dev/saas-plugin-contract": "2.4.4" + "@codefly-dev/saas-plugin-contract": "2.5.0" }, "devDependencies": { "ajv": "^8.17.1", diff --git a/module/services/frontend/code/packages/saas-plugin-react/package.json b/module/services/frontend/code/packages/saas-plugin-react/package.json index 1bf10c1b2..783676335 100644 --- a/module/services/frontend/code/packages/saas-plugin-react/package.json +++ b/module/services/frontend/code/packages/saas-plugin-react/package.json @@ -37,7 +37,7 @@ "react": ">=19.2 <20" }, "devDependencies": { - "@codefly-dev/saas-plugin-contract": "2.4.4", + "@codefly-dev/saas-plugin-contract": "2.5.0", "@testing-library/react": "^16.1.0", "@types/react": "19.2.17", "happy-dom": "^20.8.9", diff --git a/module/services/frontend/code/server/plugin-bff.ts b/module/services/frontend/code/server/plugin-bff.ts index 5f9f73eff..233605dd9 100644 --- a/module/services/frontend/code/server/plugin-bff.ts +++ b/module/services/frontend/code/server/plugin-bff.ts @@ -2,12 +2,10 @@ import { toJsonString } from "@bufbuild/protobuf"; import { type ConnectProcedure, connectProcedureTargetPath, - FRONTEND_SERVICE_ROOT_PREFIX, - type FrontendServiceAllowlistEntry, type FrontendServiceProtocol, frontendServiceTargetPath, - isConnectServiceName, isFrontendServicePath, + isServiceableFrontendService, resolveConnectProcedure, } from "@codefly-dev/saas-plugin-contract"; import { @@ -140,25 +138,6 @@ function safePath(request: Request, segments: readonly string[]): boolean { return isFrontendServicePath(segments); } -/** - * Whether this host will proxy the resolved alias at all, independent of the - * path asked for. Both rules are the composition's, re-asserted here because - * the BFF reads the GENERATED allowlist rather than the composition that - * validated it: a build-time rule no runtime re-checks is one a stale or - * hand-edited projection walks straight past. - */ -function proxyableEntry(entry: FrontendServiceAllowlistEntry): boolean { - if (entry.protocol === "connect") { - // Connect without a declared service list reaches nothing: the path - // names the service, so an absent list is an unbounded alias. - const services = entry.connectServices ?? []; - return services.length > 0 && services.every(isConnectServiceName); - } - // The origin root is Connect-only. A REST alias declaring it would be a - // prefix-less proxy of its whole upstream. - return entry.routePrefix !== FRONTEND_SERVICE_ROOT_PREFIX; -} - function sameOrigin(request: Request): boolean { const origin = request.headers.get("origin"); if (origin) { @@ -304,16 +283,37 @@ function isCapabilityProbe(path: readonly string[]): boolean { ); } +/** + * Where the fixed capability handshake is probed on this alias's upstream. + * + * On a Connect alias it lands under the requirement's own mount prefix, like + * every other procedure of that alias (issue #1121). It used to be sent to the + * origin root unconditionally, which is right only for a requirement that + * declared the root: a backend mounting Connect under `/v1/connect` answered + * the probe with its own 404, so the alias was reported incompatible and the + * mounted form this host advertises could never complete a handshake. + * + * On a REST alias the product may own the path, so the requirement's optional + * `probePath` wins over the well-known one; neither is relative to the route + * prefix, because a probe path is declared absolute and validated as such. + */ function capabilityTargetURL( baseURL: string, protocol: FrontendServiceProtocol, + routePrefix: string, probePath?: string, ): URL { const target = new URL(baseURL); - target.pathname = - protocol === "connect" - ? FRONTEND_PLUGIN_CAPABILITIES_CONNECT_PROCEDURE - : (probePath ?? FRONTEND_PLUGIN_CAPABILITIES_REST_PATH); + if (protocol !== "connect") { + target.pathname = probePath ?? FRONTEND_PLUGIN_CAPABILITIES_REST_PATH; + return target; + } + const [service, method] = + FRONTEND_PLUGIN_CAPABILITIES_CONNECT_PROCEDURE.slice(1).split("/"); + target.pathname = connectProcedureTargetPath(routePrefix, { + service, + method, + }); return target; } @@ -345,7 +345,14 @@ export async function handlePluginBffRequest( // A resolved entry the composition's own rules would have refused is a // projection this host cannot serve, not a bad request: nothing the caller // could have asked differently would make the alias proxyable. - if (!proxyableEntry(resolution.value.entry)) { + // + // `resolvePluginService` already refuses an unserviceable entry at load, so + // in this service the check below is unreachable through that resolver. + // It stays because `resolve` is a dependency: a test, or a host wiring its + // own resolver, reaches this handler with whatever entry it hands over, and + // the rule that decides what may be proxied belongs to the thing that + // proxies. One predicate, read in both places, so they cannot disagree. + if (!isServiceableFrontendService(resolution.value.entry)) { return problem("backend_unavailable", 503, requestID); } @@ -426,6 +433,7 @@ export async function handlePluginBffRequest( upstreamTarget = capabilityTargetURL( resolution.value.baseURL, resolution.value.entry.protocol, + resolution.value.entry.routePrefix, resolution.value.entry.compatibility.probePath, ); upstreamHeaders.set("accept", "application/json"); diff --git a/module/services/frontend/code/server/plugin-service-bindings.ts b/module/services/frontend/code/server/plugin-service-bindings.ts index cefa4198e..d470e3963 100644 --- a/module/services/frontend/code/server/plugin-service-bindings.ts +++ b/module/services/frontend/code/server/plugin-service-bindings.ts @@ -1,6 +1,7 @@ -import type { - FrontendServiceAllowlist, - FrontendServiceAllowlistEntry, +import { + type FrontendServiceAllowlist, + type FrontendServiceAllowlistEntry, + isServiceableFrontendService, } from "@codefly-dev/saas-plugin-contract"; import { getEndpoints, type ServiceEndpoint } from "codefly"; @@ -69,10 +70,24 @@ export function resolvePluginServiceFrom( plugin: string, alias: string, ): PluginServiceResolution { - const entry = allowlist.entries.find( - (candidate) => candidate.plugin === plugin && candidate.alias === alias, - ); + const entry = Array.isArray(allowlist?.entries) + ? allowlist.entries.find( + (candidate) => candidate.plugin === plugin && candidate.alias === alias, + ) + : undefined; if (!entry) return { ok: false, reason: "not_installed" }; + // The composition's own rules, re-checked against the GENERATED projection + // this server actually reads (issue #1121). The allowlist is a build + // artifact, so a build-time rule nothing re-checks is one a stale or + // hand-edited file walks straight past: a `rest` entry whose prefix is `""` + // is not the root the old check compared against, yet it lands every + // relative path on the upstream origin — `admin/secret` reaches + // `https://upstream/admin/secret` and is answered under the viewer's own + // bearer. An entry that cannot be served is `unavailable`, never + // `not_installed`: the alias IS installed, and this host cannot serve it. + if (!isServiceableFrontendService(entry)) { + return { ok: false, reason: "unavailable" }; + } const matches = endpoints.filter( (endpoint) => diff --git a/module/services/frontend/code/src/app/api/plugins/[plugin]/[alias]/[...path]/__tests__/route.raw-path.test.ts b/module/services/frontend/code/src/app/api/plugins/[plugin]/[alias]/[...path]/__tests__/route.raw-path.test.ts index 654c4d656..ec2468e98 100644 --- a/module/services/frontend/code/src/app/api/plugins/[plugin]/[alias]/[...path]/__tests__/route.raw-path.test.ts +++ b/module/services/frontend/code/src/app/api/plugins/[plugin]/[alias]/[...path]/__tests__/route.raw-path.test.ts @@ -12,6 +12,17 @@ import { describe, expect, it } from "vitest"; // the binding never declared, while the contract says the colon is literal // precisely because encoding it names a different resource. The raw path is the // only place that distinction still exists, so it is screened there. +// +// WHAT THIS FILE PROVES, AND WHAT IT ASSUMES (issue #1121, m3). The decoding is +// the FRAMEWORK's, and these tests hand the route its already-decoded params +// (`context(...)`) because that is the only way to call a route handler +// directly — so the decoding is the premise here, not the finding. What is +// proved is the half this repository owns: given a decoded param, the screen +// reads the RAW url and refuses, which is what a sibling test with a literal +// colon shows is a path refusal rather than an unrelated 400. The premise +// itself is the framework's documented behaviour for a catch-all segment, and +// nothing in this repository can hold it — a Next.js release that stopped +// decoding would make these refusals unreachable rather than wrong. import { GET, POST } from "@/app/api/plugins/[plugin]/[alias]/[...path]/route"; const TOKEN = `Bearer ${"a".repeat(32)}`; diff --git a/module/services/frontend/code/src/features/plugin-transport/model/demo-plugin.test.ts b/module/services/frontend/code/src/features/plugin-transport/model/demo-plugin.test.ts index 2725ca7f7..1363d0e12 100644 --- a/module/services/frontend/code/src/features/plugin-transport/model/demo-plugin.test.ts +++ b/module/services/frontend/code/src/features/plugin-transport/model/demo-plugin.test.ts @@ -46,19 +46,29 @@ describe("the transport playground's demo plugin", () => { expect(outside && transportCarries(outside)).toBe(false); }); - it("confirms every mutating route it would actually send", () => { - // The page's plugin name is editable, so a press on a carried non-read is - // a real write to whatever backend the operator named. Each one is armed - // first; a route the transport will not carry needs no confirmation - // because nothing leaves the browser. + it("confirms every non-GET route, carried or not", () => { + // The page's plugin name is editable, so a press on a non-read is a real + // write to whatever backend the operator named — and `carried` answers + // for the DEMO declaration, not for that plugin's installed requirement + // (issue #1121, N3). A row the demo reads as refused is carried the + // moment the named plugin declares that alias differently, and it left + // the browser unconfirmed. So the arming gate reads the method alone. for (const route of TRANSPORT_DEMO_ROUTES) { - expect(transportMutates(route), route.path).toBe( - route.method !== "GET" && route.carried, - ); + expect(transportMutates(route), route.path).toBe(route.method !== "GET"); } + // Non-vacuous in both directions: the table carries reads and writes, + // and at least one write the demo itself reads as refused. expect( TRANSPORT_DEMO_ROUTES.filter(transportMutates).length, ).toBeGreaterThan(0); + expect( + TRANSPORT_DEMO_ROUTES.filter((route) => !transportMutates(route)).length, + ).toBeGreaterThan(0); + expect( + TRANSPORT_DEMO_ROUTES.filter( + (route) => !route.carried && transportMutates(route), + ).length, + ).toBeGreaterThan(0); }); it("names every route against a declared alias", () => { diff --git a/module/services/frontend/code/src/features/plugin-transport/model/demo-plugin.ts b/module/services/frontend/code/src/features/plugin-transport/model/demo-plugin.ts index 571895c61..ad7d84816 100644 --- a/module/services/frontend/code/src/features/plugin-transport/model/demo-plugin.ts +++ b/module/services/frontend/code/src/features/plugin-transport/model/demo-plugin.ts @@ -172,6 +172,13 @@ export const TRANSPORT_DEMO_ROUTES: readonly TransportDemoRoute[] = [ * Whether the transport carries one demo route, derived from the contract's own * predicates rather than from the row's `carried` flag — so the table and the * transport cannot disagree about a shape while both look right. + * + * It answers for THE DEMO DECLARATION, which is what the table is about, and + * that is not the same question as what the plugin the operator named carries: + * the runtime applies the INSTALLED requirement's rule. Nothing safety-bearing + * may read this (issue #1121, N3) — `transportMutates` arms every non-GET row + * precisely because this predicate cannot answer for a plugin it has never + * seen. */ export function transportCarries(route: TransportDemoRoute): boolean { const segments = route.path.split("/"); @@ -192,9 +199,19 @@ export function transportCarries(route: TransportDemoRoute): boolean { * It matters because the page's plugin name is the operator's to edit. Pointed * at a plugin their composition really installed, a press on one of these rows * is a real write to that backend under the operator's own credential, so the - * page asks first. A route the transport will not carry needs no confirmation: - * nothing leaves the browser. + * page asks first. + * + * EVERY NON-GET ROW IS CONFIRMED, including the ones the table shows as + * refused (issue #1121, N3). It used to skip confirmation for a row + * `transportCarries` reads as refused, and that predicate answers from the DEMO + * DECLARATION — while the request is sent against the plugin the operator + * named, whose requirement for the same alias may be a different protocol with + * a different declared service list. So `procedures/records`, which the demo + * reads as a REST path on a Connect alias, is carried the moment the named + * plugin declares that alias `rest` — and it left the browser as a POST on the + * first press. A refusal the browser will make costs one extra press; a write + * nobody confirmed cannot be taken back. */ export function transportMutates(route: TransportDemoRoute): boolean { - return route.method !== "GET" && transportCarries(route); + return route.method !== "GET"; } diff --git a/module/services/frontend/code/src/features/plugin-transport/ui/plugin-transport-playground.container.test.tsx b/module/services/frontend/code/src/features/plugin-transport/ui/plugin-transport-playground.container.test.tsx index e1a4beb75..d0ec16588 100644 --- a/module/services/frontend/code/src/features/plugin-transport/ui/plugin-transport-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/plugin-transport/ui/plugin-transport-playground.container.test.tsx @@ -235,6 +235,29 @@ describe("the plugin transport playground", () => { expect(row.textContent).not.toContain("Press again to send"); }); + it("arms a non-GET row the table shows as refused", async () => { + // `carried` answers for the DEMO declaration, and the request goes to + // the plugin the operator named — whose requirement for the same alias + // may be a different protocol with a different service list (issue + // #1121, N3). So the arming gate reads the method, not the badge: this + // row is a POST the demo reads as refused, and one press must not send + // it. + const { upstream } = render(); + const row = rowFor("POST procedures/records"); + expect(row.textContent).toContain("refused"); + + press(row); + expect(row.textContent).toContain("Press again to send"); + expect(upstream).not.toHaveBeenCalled(); + // Nothing was attempted at all, so the row carries no answer yet. + expect(row.textContent).not.toContain("invalid_path"); + + press(row); + await waitFor(() => { + expect(row.textContent).toContain("invalid_path"); + }); + }); + it("sends a read on the first press", async () => { // GET is safe and idempotent, so there is nothing to confirm; making the // operator press twice for every row would turn the confirmation into diff --git a/module/services/frontend/code/src/features/plugin-transport/ui/plugin-transport-playground.tsx b/module/services/frontend/code/src/features/plugin-transport/ui/plugin-transport-playground.tsx index ee66e87ab..86bc840a2 100644 --- a/module/services/frontend/code/src/features/plugin-transport/ui/plugin-transport-playground.tsx +++ b/module/services/frontend/code/src/features/plugin-transport/ui/plugin-transport-playground.tsx @@ -83,6 +83,12 @@ type Answer = * because the custom-verb rows are writes. Anything but a read is therefore * armed by the first press, naming the plugin and alias it will write to, and * sent only by the second; editing the plugin name retracts it. + * + * EVERY non-read is armed, including the rows the Carried column shows as + * refused. That column answers for the demo declaration above, and the request + * goes to the plugin the operator named: an alias the demo declares `connect` + * may be `rest` there, so a row this page reads as refused is one that backend + * carries — and it used to leave the browser on the first press. */ export function PluginTransportPlayground() { const runtime = usePluginRuntime(); @@ -210,8 +216,10 @@ export function PluginTransportPlayground() { literal. A colon anywhere else, a traversal, a non-procedure path on a Connect alias, or a procedure of a service the requirement never declared is refused in the browser. Anything but a read asks for - confirmation first: the plugin above is editable, so a press could - write to a backend your composition really installed. + confirmation first — including a row below shown as refused, because + “carried” is read from the demo declaration while the + request goes to the plugin named above, whose own requirement for + that alias may carry it. @@ -240,7 +248,9 @@ export function PluginTransportPlayground() { - {transportCarries(route) ? "carried" : "refused"} + {transportCarries(route) + ? "carried" + : "refused by the demo"} diff --git a/module/services/frontend/code/src/lib/__tests__/plugin-bff.test.ts b/module/services/frontend/code/src/lib/__tests__/plugin-bff.test.ts index ba75e0ee6..d2a288c0c 100644 --- a/module/services/frontend/code/src/lib/__tests__/plugin-bff.test.ts +++ b/module/services/frontend/code/src/lib/__tests__/plugin-bff.test.ts @@ -529,6 +529,10 @@ describe("generic frontend plugin BFF", () => { resolve: () => ({ ok: true, value: { + // A Connect alias mounted under a path prefix, which is the + // fixture's default. The handshake is a procedure of that + // alias like any other, so it lands under the prefix + // (issue #1121); the root-prefixed form is beside it. entry: entry("connect"), baseURL: "http://example-api.internal", }, @@ -539,7 +543,7 @@ describe("generic frontend plugin BFF", () => { expect(response.status).toBe(200); const [target, init] = fetchMock.mock.calls[0]; expect(String(target)).toBe( - "http://example-api.internal/saas.frontend.plugin.v1.FrontendPluginCapabilityService/GetFrontendPluginCapabilities", + "http://example-api.internal/api/v1/example/saas.frontend.plugin.v1.FrontendPluginCapabilityService/GetFrontendPluginCapabilities", ); expect(init?.method).toBe("POST"); expect(init?.body).toBe("{}"); @@ -939,6 +943,22 @@ describe("generic frontend plugin BFF", () => { ["RecordService"], ], ["a REST alias declaring the origin root", "rest", "/", null], + // The whole prefix rule, not just the root (issue #1121). The empty + // string is NOT the root, so comparing against the root alone admitted + // it — and `frontendServiceTargetPath("", path)` lands every relative + // path on the upstream ORIGIN, under the viewer's forwarded bearer. + ["a REST alias declaring no prefix", "rest", "", null], + ["a REST alias declaring a relative prefix", "rest", "api/v1", null], + ["a REST alias declaring a traversal", "rest", "/api/../private", null], + ["a connect alias declaring no prefix", "connect", "", null], + // A non-array service list has a `length` and no `every`: the check that + // assumed an array threw out of the handler as a 500. + [ + "a connect alias whose service list is a string", + "connect", + "/", + "example.records.v1.RecordService" as unknown as readonly string[], + ], ])( "refuses to proxy %s at all", async (_label, protocol, routePrefix, connectServices) => { @@ -967,6 +987,74 @@ describe("generic frontend plugin BFF", () => { }, ); + it("refuses the review's whole-upstream probe on a prefix-less REST alias", async () => { + // The probe as the review ran it: `admin/secret` under an entry whose + // `routePrefix` is `""`. Before this it was forwarded to + // `http://example-api.internal/admin/secret` and answered 200 under the + // viewer's own bearer. The refusal is the alias's, so nothing is sent. + const fetchMock = vi.fn(async () => new Response("secret") as Response); + const response = await handlePluginBffRequest( + request("/api/plugins/example/api/admin/secret"), + { ...params, path: ["admin", "secret"] }, + dependencies({ + ...resolved("rest", ""), + fetch: fetchMock as typeof fetch, + }), + ); + expect(response.status).toBe(503); + expect(await problemCode(response)).toBe("backend_unavailable"); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("probes the capability handshake under a connect mount prefix", async () => { + // The fixed handshake is a Connect procedure like any other, so on a + // mounted alias it lands under the mount prefix (issue #1121). It used + // to be sent to the origin root unconditionally: a backend mounting + // Connect under `/v1/connect` answered its own 404, the alias was + // reported incompatible, and the mounted form this host advertises + // could never complete a handshake. + const targets: string[] = []; + const fetchMock = vi.fn(async (target: RequestInfo | URL) => { + targets.push(String(target)); + return new Response( + JSON.stringify({ + schemaVersion: 1, + contract: "example.api", + contractMajor: 1, + capabilities: [], + }), + { headers: { "content-type": "application/json" } }, + ); + }); + const response = await handlePluginBffRequest( + request("/api/plugins/example/api/.well-known/capabilities"), + capabilityParams, + dependencies({ + ...resolved("connect", "/v1/connect"), + fetch: fetchMock as typeof fetch, + }), + ); + expect(response.status).toBe(200); + expect(targets).toEqual([ + "http://example-api.internal/v1/connect/saas.frontend.plugin.v1.FrontendPluginCapabilityService/GetFrontendPluginCapabilities", + ]); + + // And a root-prefixed alias still probes the origin root, which is where + // the Connect protocol puts it. + targets.length = 0; + await handlePluginBffRequest( + request("/api/plugins/example/api/.well-known/capabilities"), + capabilityParams, + dependencies({ + ...resolved("connect", "/"), + fetch: fetchMock as typeof fetch, + }), + ); + expect(targets).toEqual([ + "http://example-api.internal/saas.frontend.plugin.v1.FrontendPluginCapabilityService/GetFrontendPluginCapabilities", + ]); + }); + it("carries a connect procedure under the mount prefix its requirement declares", async () => { // The non-root Connect prefix: the prefix says where the upstream mounts // Connect, the declared list says which of its services the alias speaks diff --git a/module/services/frontend/code/src/lib/__tests__/plugin-service-bindings.test.ts b/module/services/frontend/code/src/lib/__tests__/plugin-service-bindings.test.ts index c70384e42..a22f776b0 100644 --- a/module/services/frontend/code/src/lib/__tests__/plugin-service-bindings.test.ts +++ b/module/services/frontend/code/src/lib/__tests__/plugin-service-bindings.test.ts @@ -117,3 +117,66 @@ describe("server-only plugin service binding resolution", () => { } }); }); + +// THE GENERATED ALLOWLIST IS RE-VALIDATED AT LOAD (issue #1121). +// +// It is a build artifact, and this server reads it rather than the composition +// that validated it — so a rule nothing re-checks here is one a stale or +// hand-edited projection walks straight past. The entry is refused as +// `unavailable` rather than `not_installed`, because the alias IS installed and +// what cannot happen is this host serving it. +describe("an entry the composition's own rules would have refused", () => { + function withEntry( + overrides: Partial, + ): FrontendServiceAllowlist { + return { + ...allowlist, + entries: [{ ...allowlist.entries[0], ...overrides }], + }; + } + + it.each([ + // `""` is not the root, which is why comparing against the root alone + // admitted it — and it proxies the whole upstream origin. + ["no route prefix", { routePrefix: "" }], + ["a relative route prefix", { routePrefix: "api/v1" }], + ["a traversal in the prefix", { routePrefix: "/api/../private" }], + ["an empty segment", { routePrefix: "/api//private" }], + ["the origin root on a REST alias", { routePrefix: "/" }], + [ + "a connect alias with no declared services", + { protocol: "connect" as const, routePrefix: "/" }, + ], + [ + "a connect alias whose service list is not a list", + { + protocol: "connect" as const, + routePrefix: "/", + connectServices: + "example.records.v1.RecordService" as unknown as readonly string[], + }, + ], + ])("is unavailable: %s", (_label, overrides) => { + expect( + resolvePluginServiceFrom( + withEntry(overrides), + [endpoint()], + "example", + "api", + ), + ).toEqual({ ok: false, reason: "unavailable" }); + }); + + it("survives an allowlist whose entries are not a list", () => { + // A hand-edited or truncated projection: `find` on a non-array throws, + // and a throw out of the resolver is a 500 where a 404 is the answer. + expect( + resolvePluginServiceFrom( + { ...allowlist, entries: undefined as unknown as [] }, + [endpoint()], + "example", + "api", + ), + ).toEqual({ ok: false, reason: "not_installed" }); + }); +}); From 029342cd5c9bc7a2803943df3bb4b63cc5f96bc5 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:20:35 -0400 Subject: [PATCH 02/20] fix(accounts,frontend): a declared trigger names only a delegation its executor can use, the stopgap sweep has a switch and a visible phase, and a refused pass enqueues nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Items 5-12 of #1121, from the independent review of #1115. m5 — `DeclareSourceTrigger` sets the reference arm only for a delegation the EXECUTING module can actually use. It took the first active reference from any module, so a deployment whose executor held none read as "bound" while every occurrence was refused at the mint. What makes a delegation usable needs no new configuration: a mint by source id resolves the delegation of the CALLING module and the capability is addressed to the binding's audience, which the executing module must itself admit — so the one shape a declared reference can serve is a delegation whose binding names its own module as the audience. A delegation with no resolvable scopes is not usable either; the next mint revokes it as `binding_changed`. m2/N4 — nothing is enqueued for a pass that will be refused. The effect's binding and request digest are read first, in their own read-only transaction, because both refusals are decisions about the attempt row alone. Since the key names the organization and the source as well as the effect, a re-presented identity naming a different source produced a key of its own: a real second re-send of that source. The in-transaction check stays — it is still the decision — and `reconcileAttemptAdmits` is the one rule both ask. The `org:source:effect` key the issue asked for a test for is already held by `TestReconcileOccurrenceKeyIsScopedByOrgAndSource` from #1115, verified here by mutating the key. m1 — the delegator-==-caller exchange is tested directly. It was reached only through fixtures that happen to declare a self-audience, where a successful parse says nothing about why the audience link held. That link is the whole reason a reference is safe to hold, and for a module executing its own release the comparison is `prefix == prefix`. N5 — the page moves to `/admin/unattended-work`. The route catalog derives a page's access boundary from its path, so while it sat under `/admin/platform/` the catalog said `super_admin` and the page's gate said `admin`: two declarations for one page, disagreeing. The move makes them agree by construction rather than by a special case in the generator, and `admin-unattended-work` gives organization administrators a navigation path. N6 — the declaration reports what THIS HOST will do about the schedule, and when, as facts separate from the cron. The two share a period and not a phase: the declared cron comes due on the quarter or half hour while the sweep fires on the stored instant, which every pass moves to `now + interval`. The panel claimed they agreed, so a reader was shown 09:30 for a pass that happens at 09:07. Neither is aligned to the other on purpose — aligning the stored instant to the cron would make every source of a provider come due in the same second. N7 — `DATASOURCE_RECONCILE_SWEEP_ENABLED`, a declared key of the application group, stops this host firing the schedule it declares. It is read in two places that are one fact: the tick is not installed and the sweep itself refuses. Default ON, because a default that silently stopped every scheduled reconcile is the outage #1090 already caused. N8 — the backfill records a reference only for a module that EXECUTES declared work, and replaces the executor's stale delegation. It recorded one for every module accepting delegations, handing the administrator's authority over that source to modules whose delegation no trigger of this host could name; and it skipped any target holding an active row, so an operator over a source whose executor's binding had moved was told nothing was missing over a trigger that could never mint. A connect still delegates to every target. m3 — `source.synced` records whether anybody was present. The actor stays the delegating person, because the authority is theirs and ADR 0008 puts them in the trail deliberately; what the row could not say is that nobody asked for it, which only the layer that confirmed the delegation can tell. Payload v2, both fields optional. M4 leftover — the stale "first proof" comment on the headless mint's test is corrected: no declaration this host builds sets `Identity.Binding`, so that test proves the mint and not the arm. Co-Authored-By: Claude Opus 5 (1M context) --- module/configurations/local/application.env | 21 ++ .../proto/saas/accounts/v1/datasource.proto | 40 ++- .../proto/saas/accounts/v1/datasource.proto | 40 ++- .../datasource_trigger_identity_test.go | 18 +- .../adapters/source_operation_authority.go | 6 + .../code/pkg/business/audit_registry.go | 11 +- .../pkg/business/datasource_deliveries.go | 40 +++ .../business/datasource_deliveries_test.go | 4 +- .../datasource_reconcile_coverage_test.go | 54 ++++ .../datasource_reconcile_operation.go | 66 ++++- .../code/pkg/business/datasource_trigger.go | 90 ++++++- .../datasource_trigger_delegate_test.go | 92 ++++++- .../pkg/business/datasource_trigger_test.go | 172 ++++++++++++- .../accounts/code/pkg/business/service.go | 4 +- .../code/pkg/business/source_delegation.go | 56 ++++- .../source_delegation_self_exchange_test.go | 106 ++++++++ .../pkg/business/source_operation_receipts.go | 29 +++ .../pkg/gen/saas/accounts/v1/datasource.pb.go | 230 +++++++++++------- module/services/accounts/code/work.go | 16 ++ .../proto/saas/accounts/v1/datasource.proto | 40 ++- .../src/gen/saas/accounts/v1/datasource_pb.ts | 51 +++- .../code/src/app/admin/platform/page.tsx | 8 +- .../{platform => }/unattended-work/page.tsx | 10 + ...ource-trigger-schedules.container.test.tsx | 75 +++++- .../ui/source-trigger-schedules.tsx | 89 +++++-- ...ttended-work-playground.container.test.tsx | 13 +- .../src/gen/saas/accounts/v1/datasource_pb.ts | 51 +++- .../gen/saas/frontend/v1/plugin_catalog.ts | 3 +- .../code/tests/e2e/admin-routes-smoke.spec.ts | 2 +- .../code/tests/e2e/unattended-work.spec.ts | 4 +- .../services/frontend/code/ui-inventory.json | 2 +- .../frontend/frontend.bindings.codefly.yaml | 18 ++ .../frontend/generated/plugin-catalog.json | 27 +- 33 files changed, 1277 insertions(+), 211 deletions(-) create mode 100644 module/services/accounts/code/pkg/business/source_delegation_self_exchange_test.go rename module/services/frontend/code/src/app/admin/{platform => }/unattended-work/page.tsx (65%) diff --git a/module/configurations/local/application.env b/module/configurations/local/application.env index 586d72f66..7071c1ee7 100644 --- a/module/configurations/local/application.env +++ b/module/configurations/local/application.env @@ -34,3 +34,24 @@ APP_BASE_URL= # Interactive links use the origin above. A background-only production origin may # be supplied by deployment configuration. BOOTSTRAP_ADMIN_EMAIL= + +# Whether this host fires the datasource reconcile schedule itself. +# +# Every connected source DECLARES its reconcile schedule as a trigger a composed +# module that executes declared work is meant to fire, and this host sweeps the +# same schedule as an explicitly labelled STOPGAP, because nothing consumes that +# declaration yet (module/services/accounts/code/pkg/business/datasource_deliveries.go). +# +# While both exist they must not both run: they enqueue under different +# idempotency keys, so nothing collapses them into one job, and the per-source +# FIFO key only makes the second serialise behind the first — survivable for a +# GitHub source, a second full re-send of the whole source for a pull provider. +# So a composition that installs a module consuming the declaration sets this to +# `false`, and that is the only supported way to turn the sweep off. +# +# ON unless this reads `false`: that is what every deployment does today, and a +# default that silently stopped every scheduled reconcile would make a lost +# webhook permanent and leave a pull provider refreshing only on "Sync now". +# A declared non-empty value wins over the process environment, as with every +# key in a configuration group. +DATASOURCE_RECONCILE_SWEEP_ENABLED=true diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/datasource.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/datasource.proto index 5b952cf9d..058fbcac2 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/datasource.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/datasource.proto @@ -1034,6 +1034,16 @@ message RevokeSourceDelegationResponse { // the capability that admitted it and revoking the delegation refuses the // next occurrence. // +// IT IS SET ONLY FOR A DELEGATION THE EXECUTING MODULE CAN USE. A mint by +// source id resolves the active delegation of the CALLING module, and the +// capability is addressed to the binding's declared audience, which the +// executing module must itself admit — so the one shape a declared +// reference can serve is a delegation whose binding names its own module +// as the audience. A delegation held by one module and addressed to +// another is reachable only through the delegation-id exchange, which no +// reference this host declares can name, and declaring it would read as +// bound while every occurrence was refused at the mint. +// // IT IS NOT OPAQUE, and revocation is therefore not permanent. The value // is the source's own id, and a mint resolves whichever delegation of that // source is active, so revoking one and then RECONNECTING the source @@ -1048,11 +1058,15 @@ message RevokeSourceDelegationResponse { // binding's headless scopes — an ordinary token admission, bounded by the // capability's own short lifetime. // -// IT IS PLANNED, and no declaration this host builds has ever set it. It -// cannot serve the reconcile trigger: the headless mint lives 60 seconds -// against that trigger's 10-minute declared deadline, and the capability -// is the module's own principal while ReconcileSource requires an -// organization member. +// IT IS PLANNED, and no declaration this host builds has ever set it, so +// nothing anywhere has proved it against a trigger. It cannot serve the +// reconcile trigger: the headless mint lives 60 seconds against that +// trigger's 10-minute declared deadline, and the capability is the +// module's own principal while ReconcileSource requires an organization +// member. That mint IS the per-tenant module identity a module publishing +// for one tenant presents (PlatformAdminService.ResolveModuleTenantIdentity +// reads what it would assert); what it is not is an identity for this +// trigger's occurrences. // // THE NAMES ARE THE INSTALLATION DOCUMENT'S, not this host's and not the // executing module's read API's. That distinction is load-bearing: the module @@ -1177,6 +1191,22 @@ message SourceTriggerDeclaration { // schedule is disabled, or its interval is not one a five-field cron can // express. Empty means the declaration above is complete. string undeclarable_reason = 8; + // host_sweep_enabled says whether THIS HOST still fires the schedule it + // declares, as a labelled stopgap for the absence of a module that consumes + // the declaration, and host_sweep_due_at the instant it will next sweep on. + // + // THAT INSTANT IS NOT next_occurrence_at, and the two are reported + // separately because they share a period and not a phase. The declared cron + // for a sub-hourly interval is `*/M * * * *`, which comes due on the quarter + // or half hour; the sweep fires on the source's stored instant, which every + // pass moves to `now + interval` — an arbitrary offset inside the period. A + // reader shown only the cron's instant was reading what the declaration + // SAYS while the pass that will actually happen was at another minute. + // + // host_sweep_due_at is unset when the sweep is off, or when the source is + // held off its schedule: then there is no pass this host will take. + bool host_sweep_enabled = 9; + google.protobuf.Timestamp host_sweep_due_at = 10; } message DelegateSourceTriggersRequest { diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/datasource.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/datasource.proto index 5b952cf9d..058fbcac2 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/datasource.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/datasource.proto @@ -1034,6 +1034,16 @@ message RevokeSourceDelegationResponse { // the capability that admitted it and revoking the delegation refuses the // next occurrence. // +// IT IS SET ONLY FOR A DELEGATION THE EXECUTING MODULE CAN USE. A mint by +// source id resolves the active delegation of the CALLING module, and the +// capability is addressed to the binding's declared audience, which the +// executing module must itself admit — so the one shape a declared +// reference can serve is a delegation whose binding names its own module +// as the audience. A delegation held by one module and addressed to +// another is reachable only through the delegation-id exchange, which no +// reference this host declares can name, and declaring it would read as +// bound while every occurrence was refused at the mint. +// // IT IS NOT OPAQUE, and revocation is therefore not permanent. The value // is the source's own id, and a mint resolves whichever delegation of that // source is active, so revoking one and then RECONNECTING the source @@ -1048,11 +1058,15 @@ message RevokeSourceDelegationResponse { // binding's headless scopes — an ordinary token admission, bounded by the // capability's own short lifetime. // -// IT IS PLANNED, and no declaration this host builds has ever set it. It -// cannot serve the reconcile trigger: the headless mint lives 60 seconds -// against that trigger's 10-minute declared deadline, and the capability -// is the module's own principal while ReconcileSource requires an -// organization member. +// IT IS PLANNED, and no declaration this host builds has ever set it, so +// nothing anywhere has proved it against a trigger. It cannot serve the +// reconcile trigger: the headless mint lives 60 seconds against that +// trigger's 10-minute declared deadline, and the capability is the +// module's own principal while ReconcileSource requires an organization +// member. That mint IS the per-tenant module identity a module publishing +// for one tenant presents (PlatformAdminService.ResolveModuleTenantIdentity +// reads what it would assert); what it is not is an identity for this +// trigger's occurrences. // // THE NAMES ARE THE INSTALLATION DOCUMENT'S, not this host's and not the // executing module's read API's. That distinction is load-bearing: the module @@ -1177,6 +1191,22 @@ message SourceTriggerDeclaration { // schedule is disabled, or its interval is not one a five-field cron can // express. Empty means the declaration above is complete. string undeclarable_reason = 8; + // host_sweep_enabled says whether THIS HOST still fires the schedule it + // declares, as a labelled stopgap for the absence of a module that consumes + // the declaration, and host_sweep_due_at the instant it will next sweep on. + // + // THAT INSTANT IS NOT next_occurrence_at, and the two are reported + // separately because they share a period and not a phase. The declared cron + // for a sub-hourly interval is `*/M * * * *`, which comes due on the quarter + // or half hour; the sweep fires on the source's stored instant, which every + // pass moves to `now + interval` — an arbitrary offset inside the period. A + // reader shown only the cron's instant was reading what the declaration + // SAYS while the pass that will actually happen was at another minute. + // + // host_sweep_due_at is unset when the sweep is off, or when the source is + // held off its schedule: then there is no pass this host will take. + bool host_sweep_enabled = 9; + google.protobuf.Timestamp host_sweep_due_at = 10; } message DelegateSourceTriggersRequest { diff --git a/module/services/accounts/code/pkg/adapters/datasource_trigger_identity_test.go b/module/services/accounts/code/pkg/adapters/datasource_trigger_identity_test.go index e6280fa50..aa8a4b875 100644 --- a/module/services/accounts/code/pkg/adapters/datasource_trigger_identity_test.go +++ b/module/services/accounts/code/pkg/adapters/datasource_trigger_identity_test.go @@ -87,14 +87,18 @@ func TestTriggerOccurrenceMintsFromTheReferenceAndIsRefusedAfterRevocation(t *te require.Len(t, audit.of(business.EventSourceDelegationUsed), 2, "a refused occurrence mints nothing and records no child") } -// The installation-principal arm: a binding whose audience is the executing -// module's own and which declares headless scopes mints a capability for an -// occurrence with no person anywhere in it. +// The headless mint a binding with its own audience and headless scopes +// answers: a capability for work with no person anywhere in it. // -// It is the first proof and not the contract, and the assertions say why: the -// capability is owned and actored by the module's own principal, so there is -// no delegation to revoke and no authority to outlive the token — the run is -// bounded by the capability's own short lifetime. +// IT IS NOT THE INSTALLATION-BINDING ARM OF A DECLARED TRIGGER, and calling it +// "the first proof" of that arm was a leftover (issue #1112 M4, corrected in +// #1121). No declaration this host builds sets `Identity.Binding`, and this +// trigger cannot use one: the mint lives 60 seconds against a 10-minute +// declared deadline, and the capability is the module's own principal while +// ReconcileSource requires an organization member. What this test proves is the +// mint itself — the capability is owned and actored by the module's own +// principal, so there is no delegation to revoke and no authority to outlive +// the token, and the run is bounded by the capability's own short lifetime. func TestTriggerOccurrenceMintsFromTheInstallationBinding(t *testing.T) { store, audit := installModuleOperationContextService(t, executionPrincipals) installModuleWorkContextAuthority(t) diff --git a/module/services/accounts/code/pkg/adapters/source_operation_authority.go b/module/services/accounts/code/pkg/adapters/source_operation_authority.go index 50542c479..31795eff1 100644 --- a/module/services/accounts/code/pkg/adapters/source_operation_authority.go +++ b/module/services/accounts/code/pkg/adapters/source_operation_authority.go @@ -84,6 +84,12 @@ func authenticateSourceOperationContext(ctx context.Context, headers http.Header // requireCurrentAuthority above and borrows nobody's source. if delegated && delegation != nil && delegation.SourceID != "" { ctx = context.WithValue(ctx, sourceOperationDelegationKey{}, delegation.SourceID) + // And that nobody was present for this pass. The observation an + // operation emits records the delegating person as its actor, which is + // right — the authority is theirs — but says nothing about whether they + // asked for it; only this layer, which has just confirmed the + // delegation, can tell the two apart (issue #1121, m3). + ctx = business.WithUnattendedPass(ctx, delegation.ID) } return ctx, nil } diff --git a/module/services/accounts/code/pkg/business/audit_registry.go b/module/services/accounts/code/pkg/business/audit_registry.go index a6ac266ea..3bef12bd1 100644 --- a/module/services/accounts/code/pkg/business/audit_registry.go +++ b/module/services/accounts/code/pkg/business/audit_registry.go @@ -804,7 +804,16 @@ var auditEventCatalog = []AuditEventDefinition{ append(append(slices.Clone(sourceDelegationFields), str("audience"), strs("scopes"), boolean("lookup")), optionalFields(mintExecutionFields())...)...), 3), mutation(EventSourceDelegationRevoked, CategoryAccess, "A source delegation was revoked.", append(slices.Clone(sourceDelegationFields), enum("reason", SourceDelegationRevocationReasons...))...), - observation(EventDatasourceSourceSynced, CategorySystem, "A datasource sync was requested.", str("job_id"), str("repo")), + // v2 (#1121) records whether a person was present. A pass admitted by a + // source delegation authenticates as the DELEGATING person, so the actor is + // a `user` and that is correct rather than a mis-attribution — but a row + // that says only "a user requested a sync" cannot answer whether anybody + // asked for it. `unattended` and the delegation it was admitted under are + // optional because the other emitter of this event is a person's "Sync + // now", which carries neither. + revised(observation(EventDatasourceSourceSynced, CategorySystem, "A datasource sync was requested.", + append([]PayloadField{str("job_id"), str("repo")}, + optionalFields([]PayloadField{boolean("unattended"), uid("delegation_id")})...)...), 2), revised(mutation(EventDatasourceCredentialUpdated, CategorySystem, "A datasource credential was validated and replaced.", str("repo"), enum("credential_kind", "pat", "app", "public")), 2), // v2 names what was removed — v1 recorded an empty payload, so the trail diff --git a/module/services/accounts/code/pkg/business/datasource_deliveries.go b/module/services/accounts/code/pkg/business/datasource_deliveries.go index 0c203fb4a..be5713230 100644 --- a/module/services/accounts/code/pkg/business/datasource_deliveries.go +++ b/module/services/accounts/code/pkg/business/datasource_deliveries.go @@ -892,6 +892,40 @@ func (s *Service) allocateOrdinal(ctx context.Context, sourceID string) (int64, // source. That is the cost of a double fire, and it is why this is a stopgap // to delete and not a safety net to keep. +// SetDatasourceReconcileSweep records whether THIS deployment fires the +// reconcile schedule it declares (issue #1121, N7). +// +// It exists so the declaration's consumer and the stopgap can never both run. +// While both exist they are two schedules over one source: they enqueue under +// different idempotency keys, so nothing collapses them, and the per-source +// FIFO key only makes the second serialise behind the first — survivable for +// GitHub, a second full re-send for a pull provider. The moment a composition +// installs a module that consumes the declaration it must turn this off, and +// before this there was nothing to turn off but a redeploy of a patched host. +// +// Default ON, deliberately: that is what every deployment does today, and a +// switch whose default silently stopped every scheduled reconcile would be the +// outage #1090 already caused once. +// +// It is read in two places and they are the same fact: the tick that fires the +// sweep is not installed, and the sweep itself refuses — a host that only +// dropped the tick would still sweep from any other caller, and the +// declaration's own read would still describe a sweep that is not running. +// +// STOPGAP (issue #1112): deleted with RunDatasourceReconcile, and listed in the +// block above. +func (s *Service) SetDatasourceReconcileSweep(enabled bool) { + s.datasourceReconcileSweepOff = !enabled +} + +// DatasourceReconcileSweepEnabled reports that switch: whether this host still +// fires the reconcile schedule it declares. +// +// STOPGAP (issue #1112). +func (s *Service) DatasourceReconcileSweepEnabled() bool { + return !s.datasourceReconcileSweepOff +} + // RunDatasourceReconcile is the periodic sweep: it enqueues reconcile work for // every active source whose schedule has elapsed and reschedules it. A GitHub // source gets a conditional reconcile on the delivery queue — the job resolves @@ -910,6 +944,12 @@ func (s *Service) allocateOrdinal(ctx context.Context, sourceID string) (int64, // deletes this. func (s *Service) RunDatasourceReconcile(ctx context.Context) (int, error) { w := wool.Get(ctx).In("RunDatasourceReconcile") + // The operator turned the stopgap off, which means something else fires + // this schedule now. Nothing, not a leftover ticker and not another caller + // added later, may sweep beside it. + if !s.DatasourceReconcileSweepEnabled() { + return 0, nil + } if s.datasourceJobs == nil { return 0, w.NewError("datasource connector is not configured") } diff --git a/module/services/accounts/code/pkg/business/datasource_deliveries_test.go b/module/services/accounts/code/pkg/business/datasource_deliveries_test.go index 802a8a490..5bffaa2c0 100644 --- a/module/services/accounts/code/pkg/business/datasource_deliveries_test.go +++ b/module/services/accounts/code/pkg/business/datasource_deliveries_test.go @@ -687,7 +687,7 @@ func TestSourceTriggerDeclaration_DeclaresOnlyASourceOnItsSchedule(t *testing.T) t.Fatal(err) } - declared := business.DeclareSourceTrigger(on, nil, nil) + declared := business.DeclareSourceTrigger(on, nil, nil, true) if !declared.Declarable() { t.Fatalf("a source on its schedule declares no trigger: %q", declared.UndeclarableReason) } @@ -699,7 +699,7 @@ func TestSourceTriggerDeclaration_DeclaresOnlyASourceOnItsSchedule(t *testing.T) t.Fatalf("declared operation = %q", declared.Operation) } - if cleared := business.DeclareSourceTrigger(off, nil, nil); cleared.Declarable() { + if cleared := business.DeclareSourceTrigger(off, nil, nil, true); cleared.Declarable() { t.Fatalf("a source off its schedule declared a trigger: %+v", cleared.Schedule) } } diff --git a/module/services/accounts/code/pkg/business/datasource_reconcile_coverage_test.go b/module/services/accounts/code/pkg/business/datasource_reconcile_coverage_test.go index 3955b29cf..66432a10f 100644 --- a/module/services/accounts/code/pkg/business/datasource_reconcile_coverage_test.go +++ b/module/services/accounts/code/pkg/business/datasource_reconcile_coverage_test.go @@ -180,6 +180,60 @@ func TestReconcileSourceOperation_TwoAttemptsOfOneOccurrenceAreOneJob(t *testing } } +// NOTHING IS ENQUEUED FOR A PASS THAT WILL BE REFUSED (issue #1121, m2/N4). +// +// The effect's binding and digest are read before the enqueue, because both +// refusals are decisions about the attempt row alone. Enqueueing and then +// refusing left a job behind for a request this host rejected — and since the +// key names the source, a re-presented identity naming a DIFFERENT source +// produced a key of its own: a real second re-send of that source rather than +// a collapsed replay. +func TestReconcileSourceOperation_RefusedPassEnqueuesNothing(t *testing.T) { + producer := &recordingProducer{} + store := newReconcileOperationStore() + svc, _ := newDatasourceService(store, producer, &fakeGitHub{defaultBranch: "main", commit: "abc"}) + + first := newUploadSource(t, svc) + second := newCrawlerSource(t, svc) + takeConnectSync(t, producer, first) + takeConnectSync(t, producer, second) + + // One occurrence of the first source, admitted and recorded. + reconcileOnce(t, svc, first.OrgID, first.ID, "occurrence-1") + if got := len(jobKeys(producer, first.ID)); got != 1 { + t.Fatalf("the admitted pass enqueued %d job(s), want 1", got) + } + + // The same identity, now naming the second source. It is refused — and the + // refusal must happen before anything reaches the queue. + digest := sha256.Sum256([]byte("occurrence-1")) + ctx := receipts.WithEffect(t.Context(), receipts.Effect{ + ID: "occurrence-1", Tenant: second.OrgID, Method: business.ReconcileSourceMethod, RequestDigest: digest[:], + }) + err := svc.ReconcileDatasourceSource(ctx, "actor", second.OrgID, second.ID, + func(context.Context, string) error { return nil }) + if err == nil { + t.Fatal("an effect identity bound to another source was admitted") + } + if jobs := jobKeys(producer, second.ID); len(jobs) != 0 { + t.Fatalf("a refused pass enqueued %d job(s) for the second source: %v", len(jobs), jobs) + } + + // And the same source with a different request digest: refused as a reuse, + // again with nothing enqueued beyond the one admitted pass. + other := sha256.Sum256([]byte("a different request")) + ctx = receipts.WithEffect(t.Context(), receipts.Effect{ + ID: "occurrence-1", Tenant: first.OrgID, Method: business.ReconcileSourceMethod, RequestDigest: other[:], + }) + if err := svc.ReconcileDatasourceSource(ctx, "actor", first.OrgID, first.ID, + func(context.Context, string) error { return nil }); err == nil { + t.Fatal("an effect identity re-presented with another request was admitted") + } + if got := len(jobKeys(producer, first.ID)); got != 1 { + t.Fatalf("a refused reuse enqueued a second job: %d", got) + } +} + // reconcileOnce runs one reconcile pass as one occurrence of the declared // trigger would: an effect identity in context, and the receipt committed by // the callback the operation hands it. diff --git a/module/services/accounts/code/pkg/business/datasource_reconcile_operation.go b/module/services/accounts/code/pkg/business/datasource_reconcile_operation.go index b9c076560..46ddedcef 100644 --- a/module/services/accounts/code/pkg/business/datasource_reconcile_operation.go +++ b/module/services/accounts/code/pkg/business/datasource_reconcile_operation.go @@ -71,6 +71,28 @@ func (s *Service) ReconcileDatasourceSource(ctx context.Context, actor, org, sou if !ok { return operationUnavailable() } + // NOTHING IS ENQUEUED FOR A PASS THAT WILL BE REFUSED (issue #1121, m2/N4). + // The effect's binding and request digest are read FIRST, in their own + // read-only transaction, because both refusals below are decisions about + // the attempt row alone: enqueueing and then refusing left a job behind for + // a request this host rejected, and since the key now names the + // organization and the source as well as the effect, a re-presented + // identity naming a different source produced a key of its own — a real + // second re-send of that source rather than a collapsed replay. + // + // This is not a second authority. The transaction below is still the + // decision: a concurrent attempt can take the row between the two reads, so + // the checks are made again where they commit. What this read adds is that + // the queue is not touched by a request already known to be refused. + if err := s.store.WithOrgTx(ctx, org, func(ctx context.Context) error { + prior, err := attempts.GetSourceOperationAttempt(ctx, org, effect.ID) + if err != nil { + return operationUnavailable() + } + return reconcileAttemptAdmits(prior, actor, source.ID, effect.RequestDigest) + }); err != nil { + return err + } // The enqueue comes before the record, and the record is what makes the // pass recoverable. That order is safe only because the enqueue is keyed by // the effect: a crash between the two leaves one job that a retry under the @@ -84,8 +106,10 @@ func (s *Service) ReconcileDatasourceSource(ctx context.Context, actor, org, sou if err != nil { return operationUnavailable() } - switch { - case prior == nil: + if err := reconcileAttemptAdmits(prior, actor, source.ID, effect.RequestDigest); err != nil { + return err + } + if prior == nil { created, err := attempts.CreateSourceOperationAttempt(ctx, SourceOperationAttempt{ OrgID: org, EffectID: effect.ID, ActorID: actor, SourceID: source.ID, Operation: reconcileSourceOperation, DeclarationDigest: ReconcileSourceMethod, @@ -94,10 +118,6 @@ func (s *Service) ReconcileDatasourceSource(ctx context.Context, actor, org, sou if err != nil || !created { return operationUnavailable() } - case prior.ActorID != actor || prior.SourceID != source.ID || prior.Operation != reconcileSourceOperation: - return operationRefused("SOURCE_EFFECT_BINDING_CHANGED") - case !bytes.Equal(prior.RequestDigest, effect.RequestDigest): - return operationRefused("SOURCE_EFFECT_REUSED") } return commit(ctx, jobID) }) @@ -111,8 +131,18 @@ func (s *Service) ReconcileDatasourceSource(ctx context.Context, actor, org, sou // durability rule rather than an oversight: "a sync was requested" observes // a handover that has already happened, so rolling it back with the write // it observes would lose the record of work that is still going to run. - s.emit(ctx, actor, "user", EventDatasourceSourceSynced, "datasource", source.ID, org, - map[string]any{"job_id": jobID, "repo": source.Repo}) + // + // The actor stays the person, because for a delegated pass the authority IS + // that person's and ADR 0008 puts them in the trail on purpose. What the + // row now also says is whether anybody was present: an unattended pass + // names the delegation it was admitted under (issue #1121, m3), so a reader + // can tell one from a person's "Sync now" without joining the mint trail + // by hand. + payload := map[string]any{"job_id": jobID, "repo": source.Repo} + if delegationID, unattended := unattendedPass(ctx); unattended { + payload["unattended"], payload["delegation_id"] = true, delegationID + } + s.emit(ctx, actor, "user", EventDatasourceSourceSynced, "datasource", source.ID, org, payload) // The schedule's own projection moves with the pass, not with a clock this // host runs: the stored instant is what the park and restore paths read as // "this source is on its schedule", and a pass that left it alone would let @@ -122,6 +152,26 @@ func (s *Service) ReconcileDatasourceSource(ctx context.Context, actor, org, sou }) } +// reconcileAttemptAdmits judges one effect identity against the attempt row +// that already holds it: absent is a first attempt, an identical binding and +// request digest is a retry of the same occurrence, and anything else is a +// refusal. +// +// One function because it is asked twice — once before the enqueue, once where +// the receipt commits — and two copies of a refusal rule are two places for the +// pre-check to start admitting what the commit refuses. +func reconcileAttemptAdmits(prior *SourceOperationAttempt, actor, sourceID string, requestDigest []byte) error { + switch { + case prior == nil: + return nil + case prior.ActorID != actor || prior.SourceID != sourceID || prior.Operation != reconcileSourceOperation: + return operationRefused("SOURCE_EFFECT_BINDING_CHANGED") + case !bytes.Equal(prior.RequestDigest, requestDigest): + return operationRefused("SOURCE_EFFECT_REUSED") + } + return nil +} + // enqueueReconcileOccurrence hands one pass to the engine the source's provider // actually syncs on, keyed by the effect so a retry is the same job. // diff --git a/module/services/accounts/code/pkg/business/datasource_trigger.go b/module/services/accounts/code/pkg/business/datasource_trigger.go index 5e8c60de3..d35b92e44 100644 --- a/module/services/accounts/code/pkg/business/datasource_trigger.go +++ b/module/services/accounts/code/pkg/business/datasource_trigger.go @@ -169,8 +169,35 @@ type SourceTriggerDeclaration struct { // source, whatever admitted it. NextOccurrenceAt is the next instant the // declared cron comes due, computed from the declaration — never a stored // cursor, because the cursor belongs to the module that fires the trigger. - LastOccurrenceAt *time.Time - NextOccurrenceAt *time.Time + LastOccurrenceAt *time.Time + NextOccurrenceAt *time.Time + // HostSweepEnabled and HostSweepDueAt say what THIS HOST will do about the + // schedule, which is not the same question as what the declaration says + // (issue #1121, N6). + // + // The two share a period and NOT a phase, and reporting only the cron's + // instant said otherwise. The declared cron for a sub-hourly interval is + // `*/M * * * *`, which comes due on the quarter- or half-hour; the stopgap + // sweep fires on the STORED instant, which every pass and every bump moves + // to `now + interval` — an arbitrary offset inside the period, set by + // whenever the source was last connected or reconciled. So a reader shown + // "next occurrence 14:30" was reading the declaration's statement while the + // pass that will actually happen was at 14:07. + // + // Keeping the cron's phase and the stored instant apart is deliberate: the + // cron must not be aligned to the stored instant (the declaration is + // derived from the interval and nothing else), and the stored instant must + // not be aligned to the cron (`*/M` has no per-source spread, so every + // source of a provider would come due in the same second — a thundering + // herd on one provider, which is exactly what the sweep's natural + // scattering avoids). They are two facts, so they are reported as two. + // + // HostSweepDueAt is unset when the sweep is off, or when the source is not + // on its schedule: then there is no pass this host will take. + // + // STOPGAP (issue #1112): both go with RunDatasourceReconcile. + HostSweepEnabled bool + HostSweepDueAt *time.Time UndeclarableReason string } @@ -202,6 +229,16 @@ type TriggerDelegationReference struct { RevokedReason string } +// usableByExecutor reports whether a declared trigger's reference arm can name +// this delegation: it is live, it has a reference value, its binding still +// confers something, and the module that holds it is the audience of its own +// binding — the only shape the module that fires the trigger can both mint from +// a source id and admit as its own execution context (DeclareSourceTrigger). +func (r TriggerDelegationReference) usableByExecutor() bool { + return r.Active && r.Reference != "" && len(r.Scopes) > 0 && + r.Audience != "" && r.Audience == r.Module +} + // TriggerChildContext is one child Work Context this host minted against a // reference, as the audit trail recorded it. There is no other record to read: // a capability lives about a minute and nothing caches one. @@ -352,10 +389,33 @@ func datasourceTriggerInput(orgID, sourceID string) []byte { // stated as planned in MODULE.md and WORK_CONTEXTS.md rather than quietly // shortened to fit. // +// ONLY A DELEGATION THE EXECUTING MODULE CAN ACTUALLY USE IS NAMED (issue +// #1121, m5). It used to take the first active reference from any module, so a +// deployment whose executing module holds none read as "bound" because some +// other module's delegation existed — and every occurrence was then refused at +// the mint, with the page saying the opposite. +// +// What makes a delegation usable by the module that fires the trigger is +// derivable from the delegation itself, and needs no new configuration: the +// reference arm is a source id, a mint by source id resolves the active +// delegation OF THE CALLING MODULE (ActiveSourceDelegation by source and +// prefix), and the capability it answers with is addressed to the binding's +// declared audience — which the executing module must itself admit, because it +// attaches the capability as its own incoming execution context. Both are true +// of exactly one shape: a delegation whose binding names its own module as the +// audience, which is the self-admitting binding selfAdmittingBinding exists to +// allow. A delegation to module A addressed to audience B is usable only +// through the delegation-id exchange, which a source-id reference cannot reach. +// +// An active delegation with no resolvable scopes is not one either: the module +// no longer declares the binding it was recorded against, so the next mint +// revokes it as binding_changed. +// // THE REFERENCE IS THE SOURCE ID, AND REVOCATION IS THEREFORE NOT DURABLE // (issue #1112, M3). Every delegation on one source reads back under the same -// reference value, so this loop names whichever module holds an active one; -// and because a mint resolves the active delegation from the source afresh, a +// reference value, so the resolution below cannot distinguish two delegations +// of one source by reference value; and because a mint resolves the active +// delegation from the source afresh, a // reconnect that records a new delegation re-arms an installed trigger with no // explicit re-delegation. The durable shape is an opaque, delegation-level // reference, which this host's own mint already accepts (SourceDelegationRef's @@ -365,16 +425,27 @@ func datasourceTriggerInput(orgID, sourceID string) []byte { // from what it sent, so a delegation id in this field is rejected by the // caller, not by this host. Named in WORK_CONTEXTS.md § Triggers as that // facade's gap. -func DeclareSourceTrigger(source *DatasourceSource, references []TriggerDelegationReference, lastOccurrence *time.Time) SourceTriggerDeclaration { +func DeclareSourceTrigger( + source *DatasourceSource, references []TriggerDelegationReference, + lastOccurrence *time.Time, hostSweep bool, +) SourceTriggerDeclaration { declaration := SourceTriggerDeclaration{ Release: DatasourceReconcileRelease, Operation: DatasourceReconcileOperation, LastOccurrenceAt: lastOccurrence, + HostSweepEnabled: hostSweep, } if source == nil { declaration.UndeclarableReason = TriggerUndeclarableNoSchedule return declaration } + // The instant the stopgap will sweep on, which is the stored one and not + // the cron's — see the field's note. A source held off its schedule has a + // cleared instant, so there is no pass to report. + if hostSweep && source.Status == DatasourceStatusActive && source.NextReconcileAt != nil { + due := source.NextReconcileAt.UTC() + declaration.HostSweepDueAt = &due + } cron, reason := datasourceTriggerCron(source.ReconcileInterval, source.ID) if reason != "" { declaration.UndeclarableReason = reason @@ -396,10 +467,10 @@ func DeclareSourceTrigger(source *DatasourceSource, references []TriggerDelegati Deadline: DatasourceTriggerDeadline, Input: datasourceTriggerInput(source.OrgID, source.ID), } - // Whichever module holds an active delegation, because every delegation on - // one source carries the same reference value — see the M3 note above. + // The executing module's own delegation, and nothing else — see the m5 note + // above for why that is the self-admitting one. for _, reference := range references { - if reference.Active && reference.Reference != "" { + if reference.usableByExecutor() { declaration.Identity = DeclaredTriggerIdentity{Source: reference.Reference} break } @@ -586,7 +657,8 @@ func (s *Service) GetSourceTriggerDeclaration(ctx context.Context, orgID, source if err != nil { return SourceTriggerDeclaration{}, nil, nil, err } - return DeclareSourceTrigger(source, references, source.LastSyncedAt), references, children, nil + return DeclareSourceTrigger(source, references, source.LastSyncedAt, s.DatasourceReconcileSweepEnabled()), + references, children, nil } // DelegateSourceTriggers records the delegation a declared release's triggers diff --git a/module/services/accounts/code/pkg/business/datasource_trigger_delegate_test.go b/module/services/accounts/code/pkg/business/datasource_trigger_delegate_test.go index 602ba6ebd..8136d0772 100644 --- a/module/services/accounts/code/pkg/business/datasource_trigger_delegate_test.go +++ b/module/services/accounts/code/pkg/business/datasource_trigger_delegate_test.go @@ -25,9 +25,21 @@ import ( "github.com/stretchr/testify/require" ) -// Two modules, each with one binding that accepts a delegation. Two rather -// than one because the harm is to the delegation this call did NOT come to -// record. +// Three modules, each with one binding that accepts a delegation, and they are +// not interchangeable. +// +// `docstore` and `reportstore` are CONSUMING modules: their bindings name +// another service as the audience, which is what a person's connect delegates +// to. `runtime` is the module that EXECUTES declared work: its binding names +// its own prefix, because it mints the capability for an occurrence and then +// admits that capability as its own incoming execution context. +// +// The distinction is what the backfill is narrowed to (issue #1121, N8): a +// trigger's reference arm is a source id, a mint by source id resolves the +// delegation of the CALLING module, and the capability is addressed to the +// binding's audience — so only the self-admitting shape can serve one. A +// delegation recorded for a consuming module is the administrator's authority +// handed to a module whose delegation no trigger of this host could name. const delegateTriggerPrincipals = `{` + `"docstore":{"workload":{"service_account":"docstore","namespace":"acme-prod","container":"app"},` + `"tenant":"` + testOrg + `","operation_audiences":{"source-sync":{"audience":"ingestservice",` + @@ -36,6 +48,11 @@ const delegateTriggerPrincipals = `{` + `"reportstore":{"workload":{"service_account":"reportstore","namespace":"acme-prod","container":"app"},` + `"tenant":"` + testOrg + `","operation_audiences":{"source-sync":{"audience":"reportservice",` + `"invoke_scopes":[{"resource_kind":"collections","actions":["read","write"]}],` + + `"source_delegation_scopes":[{"resource_kind":"collections","actions":["write"]}]}}},` + + `"runtime":{"workload":{"service_account":"runtime","namespace":"acme-prod","container":"app"},` + + `"tenant":"` + testOrg + `","operation_audiences":{"occurrences":{"audience":"runtime",` + + `"invoke_scopes":[{"resource_kind":"collections","actions":["read","write"]}],` + + `"headless_scopes":[{"resource_kind":"collections","actions":["read"]}],` + `"source_delegation_scopes":[{"resource_kind":"collections","actions":["write"]}]}}}}` // A delegation another module already holds survives an administrator's @@ -70,14 +87,70 @@ func TestDelegateSourceTriggersLeavesAnotherModulesDelegationAlone(t *testing.T) require.Equal(t, "connecting-member", after.PrincipalID, "an administrator's reference operation re-owned another module's delegation") - // And the module that had none got one — that IS the point of the call. - recorded := store.activeFor(source.ID, "reportstore") - require.NotNil(t, recorded, "the module with no delegation did not get one") + // And the EXECUTING module, which had none, got one — that IS the point of + // the call: the reference a declared release's trigger names. + recorded := store.activeFor(source.ID, "runtime") + require.NotNil(t, recorded, "the executing module with no delegation did not get one") require.Equal(t, "an-administrator", recorded.PrincipalID) - // Exactly one delegation per module, and no replacement anywhere. + + // The other CONSUMING module did not (issue #1121, N8). It accepts + // delegations, and it had none, and the old backfill recorded one anyway — + // handing the administrator's authority over this source to a module whose + // delegation no trigger of this host can name. A person's connect is what + // delegates to a consuming module, and that path is unchanged. + require.Nil(t, store.activeFor(source.ID, "reportstore"), + "the backfill recorded a reference for a module that executes no declared work") + + // No replacement anywhere. require.Equal(t, 0, store.revocations, "a backfill revoked something") } +// A delegation the executor's own current declaration no longer supports is +// not "nothing was missing" (issue #1121, N8). +// +// A delegation carries a digest of its binding's audience and delegation +// scopes, and the very next mint revokes one whose binding has changed +// (`binding_changed`). The backfill used to skip any target holding an active +// row, so an operator pressing the button over a source whose executor's +// binding had moved was told nothing was missing — over a trigger that could +// never mint. It is revoked here, under the same reason the mint would use, and +// a fresh one is recorded in the same transaction. +func TestDelegateSourceTriggersReplacesTheExecutorsStaleDelegation(t *testing.T) { + store := newDelegateTriggerStore(t) + svc, _ := newDatasourceService(store, &recordingProducer{}, &fakeGitHub{defaultBranch: "main", commit: "abc"}) + source := githubSource(t, svc, "main", nil, "") + registry, err := moduleregistrytest.Parse(delegateTriggerPrincipals) + require.NoError(t, err) + svc.SetModuleCapabilities(nil, nil, registry) + + // The executor's delegation as recorded against an EARLIER shape of its + // binding: same binding id, a digest that no longer matches. + stale := store.insert(source.ID, "connecting-member", "runtime", registry) + stale.BindingDigest = "0" + stale.BindingDigest[1:] + + _, err = svc.DelegateSourceTriggers(context.Background(), "an-administrator", testOrg, source.ID) + require.NoError(t, err) + + gone := store.get(stale.ID) + require.NotNil(t, gone) + require.False(t, gone.Active(), "a delegation the next mint would revoke was left in place") + require.Equal(t, business.SourceDelegationBindingChanged, gone.RevokedReason, + "it is revoked for the reason the mint itself would record") + + fresh := store.activeFor(source.ID, "runtime") + require.NotNil(t, fresh, "nothing replaced the unusable delegation") + require.NotEqual(t, stale.ID, fresh.ID) + require.Equal(t, "an-administrator", fresh.PrincipalID) + + // And a delegation that IS current is still left exactly as it is: the + // revocation above is not the connect path creeping back. + store.revocations = 0 + _, err = svc.DelegateSourceTriggers(context.Background(), "another-administrator", testOrg, source.ID) + require.NoError(t, err) + require.Equal(t, 0, store.revocations, "a current delegation was replaced") + require.Equal(t, fresh.ID, store.activeFor(source.ID, "runtime").ID) +} + // A personal source is its owner's. An administrator is refused rather than // recording a reference whose authority is theirs. // @@ -106,7 +179,10 @@ func TestDelegateSourceTriggersRefusesAnotherMembersPersonalSource(t *testing.T) // reference borrows is exactly theirs. _, err = svc.DelegateSourceTriggers(context.Background(), "the-owner", testOrg, source.ID) require.NoError(t, err) - require.Len(t, store.delegations, 2, "the owner's own reference operation recorded nothing") + // One row: the executing module's. The two consuming modules get nothing + // from a backfill (issue #1121, N8). + require.Len(t, store.delegations, 1, "the owner's own reference operation recorded nothing") + require.NotNil(t, store.activeFor(source.ID, "runtime")) } // delegateTriggerStore is the datasource fake plus the delegation rows and the diff --git a/module/services/accounts/code/pkg/business/datasource_trigger_test.go b/module/services/accounts/code/pkg/business/datasource_trigger_test.go index 58722c0e7..d81d82f38 100644 --- a/module/services/accounts/code/pkg/business/datasource_trigger_test.go +++ b/module/services/accounts/code/pkg/business/datasource_trigger_test.go @@ -140,17 +140,17 @@ func TestTriggerScheduleSpellsOnlyTheIntervalsCronCanName(t *testing.T) { 15 * time.Minute: "*/15 * * * *", time.Minute: "*/1 * * * *", } { - declared := business.DeclareSourceTrigger(source(interval), nil, nil) + declared := business.DeclareSourceTrigger(source(interval), nil, nil, true) require.True(t, declared.Declarable(), "%s: %s", interval, declared.UndeclarableReason) require.Equal(t, want, declared.Schedule.Cron, "interval %s", interval) } // A day, and a divisor of a day, are expressible; the minute (and hour) are // derived from the source so every source of one provider does not come due // in the same second. - daily := business.DeclareSourceTrigger(source(24*time.Hour), nil, nil) + daily := business.DeclareSourceTrigger(source(24*time.Hour), nil, nil, true) require.True(t, daily.Declarable()) require.Regexp(t, `^\d+ \d+ \* \* \*$`, daily.Schedule.Cron) - twelveHourly := business.DeclareSourceTrigger(source(12*time.Hour), nil, nil) + twelveHourly := business.DeclareSourceTrigger(source(12*time.Hour), nil, nil, true) require.True(t, twelveHourly.Declarable()) require.Regexp(t, `^\d+ \*/12 \* \* \*$`, twelveHourly.Schedule.Cron) @@ -161,11 +161,11 @@ func TestTriggerScheduleSpellsOnlyTheIntervalsCronCanName(t *testing.T) { 5 * time.Hour, // does not divide a day 48 * time.Hour, // beyond a day, which cron names by date, not by period } { - declared := business.DeclareSourceTrigger(source(interval), nil, nil) + declared := business.DeclareSourceTrigger(source(interval), nil, nil, true) require.False(t, declared.Declarable(), "interval %s was spelled %q", interval, declared.Schedule.Cron) require.Equal(t, business.TriggerUndeclarableInterval, declared.UndeclarableReason, "interval %s", interval) } - disabled := business.DeclareSourceTrigger(&business.DatasourceSource{ID: "s", OrgID: "org", Status: "active"}, nil, nil) + disabled := business.DeclareSourceTrigger(&business.DatasourceSource{ID: "s", OrgID: "org", Status: "active"}, nil, nil, true) require.Equal(t, business.TriggerUndeclarableNoSchedule, disabled.UndeclarableReason) } @@ -179,20 +179,23 @@ func TestTriggerDeclarationPrefersTheReferenceArmAndFingerprintsItself(t *testin ID: "2a3f5c1e-0000-4000-8000-000000000002", OrgID: "org", Status: "active", ReconcileInterval: 30 * time.Minute, NextReconcileAt: &next, } - live := []business.TriggerDelegationReference{{Reference: source.ID, DelegationID: "d1", Active: true}} - revoked := []business.TriggerDelegationReference{{Reference: source.ID, DelegationID: "d1", Active: false}} + // The executing module's own delegation: its binding names itself as the + // audience, which is the only shape a source-id reference can serve (see + // executorReference below). + live := []business.TriggerDelegationReference{executorReference(source.ID, true)} + revoked := []business.TriggerDelegationReference{executorReference(source.ID, false)} - referenced := business.DeclareSourceTrigger(source, live, nil) + referenced := business.DeclareSourceTrigger(source, live, nil, true) require.Equal(t, source.ID, referenced.Identity.Source) require.True(t, referenced.Identity.Reference()) // A revoked reference is not one: the declaration names no identity, which // an installing module refuses rather than running the work as itself. - withoutIdentity := business.DeclareSourceTrigger(source, revoked, nil) + withoutIdentity := business.DeclareSourceTrigger(source, revoked, nil, true) require.False(t, withoutIdentity.Identity.Declared()) source.ReconcileInterval = 15 * time.Minute - edited := business.DeclareSourceTrigger(source, live, nil) + edited := business.DeclareSourceTrigger(source, live, nil, true) require.NotEqual(t, referenced.Digest, edited.Digest, "an interval edit re-declares the trigger") // The next occurrence is computed from the declared cron, and nothing else: @@ -202,6 +205,76 @@ func TestTriggerDeclarationPrefersTheReferenceArmAndFingerprintsItself(t *testin require.Zero(t, edited.NextOccurrenceAt.Minute()%15) } +// executorReference is a delegation the module that FIRES the trigger can +// actually use: it holds the delegation itself, its binding still confers +// something, and that binding names its own module as the audience. +func executorReference(sourceID string, active bool) business.TriggerDelegationReference { + return business.TriggerDelegationReference{ + Reference: sourceID, DelegationID: "d1", Module: "runtime", Binding: "occurrences", + Audience: "runtime", Scopes: []string{"collections:write"}, Active: active, + } +} + +// ONLY THE EXECUTING MODULE'S DELEGATION IS NAMED (issue #1121, m5). +// +// The reference arm is a source id, and a mint by source id resolves the +// active delegation OF THE CALLING MODULE — so a delegation held by a module +// that does not fire this trigger can never admit one of its occurrences. It +// used to be taken anyway, as long as it was active and had a reference value, +// which is the worst shape a declaration can be in: the page reads "bound" and +// every occurrence is refused at the mint. +// +// Each case below is one of the three facts the resolution needs, removed on +// its own, so no single assertion carries the test. +func TestTriggerDeclarationNamesOnlyADelegationItsExecutorCanUse(t *testing.T) { + next := time.Now().Add(time.Hour) + source := &business.DatasourceSource{ + ID: "2a3f5c1e-0000-4000-8000-000000000003", OrgID: "org", + Status: "active", ReconcileInterval: 30 * time.Minute, NextReconcileAt: &next, + } + + usable := executorReference(source.ID, true) + require.Equal(t, source.ID, business.DeclareSourceTrigger(source, []business.TriggerDelegationReference{usable}, nil, true).Identity.Source) + + // A delegation held by one module and addressed to another. It is a real, + // live delegation — the shape every consuming module's sync uses — and it + // is reachable only through the delegation-id exchange, which no reference + // this host declares can name. + addressedElsewhere := usable + addressedElsewhere.Audience = "somebody-else" + + // A delegation whose binding the module no longer declares: the read + // resolves no scopes for it, and the next mint revokes it as + // binding_changed. "Active" is not the same as "usable". + bindingGone := usable + bindingGone.Scopes = nil + + // And a delegation whose authority could not be resolved at all, which the + // read reports as an empty audience rather than by omitting the row. + unresolved := usable + unresolved.Audience = "" + + for name, references := range map[string][]business.TriggerDelegationReference{ + "another module's delegation": {addressedElsewhere}, + "a binding no longer declared": {bindingGone}, + "an authority that did not resolve": {unresolved}, + // Several unusable ones do not add up to a usable one. + "all of them together": {addressedElsewhere, bindingGone, unresolved}, + } { + t.Run(name, func(t *testing.T) { + declared := business.DeclareSourceTrigger(source, references, nil, true) + require.True(t, declared.Declarable(), "the schedule is still declared; only the identity is missing") + require.False(t, declared.Identity.Declared(), + "a declaration naming an identity its executor cannot mint reads as bound and refuses every occurrence") + }) + } + + // The usable one is found whatever order the read returns, and the + // unusable ones never shadow it. + mixed := business.DeclareSourceTrigger(source, []business.TriggerDelegationReference{addressedElsewhere, usable}, nil, true) + require.Equal(t, source.ID, mixed.Identity.Source) +} + // THE STOPGAP SWEEP IS CONFINED TO THE FILES THAT DELETE IT (issue #1112). // // The host fires this schedule again, as a labelled stopgap, because nothing @@ -386,18 +459,93 @@ func TestStopgapSweepLeavesTheDeclarationUnchanged(t *testing.T) { source := githubSource(t, svc, "main", nil, "") setNextReconcile(t, store, source.ID, time.Now().Add(-time.Minute)) - before := business.DeclareSourceTrigger(storedSource(t, store, source.ID), nil, nil) + before := business.DeclareSourceTrigger(storedSource(t, store, source.ID), nil, nil, true) require.True(t, before.Declarable()) _, err := svc.RunDatasourceReconcile(t.Context()) require.NoError(t, err) - after := business.DeclareSourceTrigger(storedSource(t, store, source.ID), nil, nil) + after := business.DeclareSourceTrigger(storedSource(t, store, source.ID), nil, nil, true) require.True(t, after.Declarable(), "the sweep took the source off its declared schedule") require.Equal(t, before.Schedule.Cron, after.Schedule.Cron) require.Equal(t, before.Digest, after.Digest, "the sweep moved the declaration an installation pinned") } +// THE STOPGAP SWEEP HAS A SWITCH, AND IT IS ONE FACT (issue #1121, N7). +// +// The declaration's consumer and the sweep must never both run: they enqueue +// under different idempotency keys, so nothing collapses them, and the +// per-source FIFO key only makes the second serialise behind the first — a +// second full re-send for a pull provider. Before this there was nothing to +// turn off but a redeployed, patched host. +// +// The switch is read in two places and they must agree: the tick is not +// installed AND the sweep itself refuses. A host that only dropped the tick +// would still sweep from any other caller, and would still describe a sweep +// that is not running. +func TestTheStopgapSweepCanBeSwitchedOff(t *testing.T) { + producer := &recordingProducer{} + store := newDatasourceFakeStore() + svc, _ := newDatasourceService(store, producer, &fakeGitHub{defaultBranch: "main", commit: "abc"}) + + source := githubSource(t, svc, "main", nil, "") + due := time.Now().Add(-time.Minute) + setNextReconcile(t, store, source.ID, due) + + // Default ON: that is what every deployment does today, and a default that + // silently stopped every scheduled reconcile is the outage #1090 caused. + require.True(t, svc.DatasourceReconcileSweepEnabled()) + + svc.SetDatasourceReconcileSweep(false) + require.False(t, svc.DatasourceReconcileSweepEnabled()) + enqueued, err := svc.RunDatasourceReconcile(t.Context()) + require.NoError(t, err, "a disabled sweep is not an error; it is a sweep that does not happen") + require.Zero(t, enqueued) + require.Empty(t, jobKeys(producer, source.ID), "a disabled sweep enqueued work") + + // And it did not move the source's schedule either: a host that is not + // firing must not consume the instant something else fires on. + require.Equal(t, due.UTC().Truncate(time.Second), + storedSource(t, store, source.ID).NextReconcileAt.UTC().Truncate(time.Second), + "a disabled sweep bumped the schedule") + + // The declaration says which of the two is acting, and the instant each + // acts on. They share a period and NOT a phase: the cron comes due on the + // half hour while the sweep fires on the stored instant. + off := business.DeclareSourceTrigger(storedSource(t, store, source.ID), nil, nil, false) + require.False(t, off.HostSweepEnabled) + require.Nil(t, off.HostSweepDueAt, "a host that takes no pass must not name an instant for one") + require.NotNil(t, off.NextOccurrenceAt, "the declaration is unchanged by the switch") + + on := business.DeclareSourceTrigger(storedSource(t, store, source.ID), nil, nil, true) + require.True(t, on.HostSweepEnabled) + require.NotNil(t, on.HostSweepDueAt) + require.Equal(t, due.UTC().Truncate(time.Second), on.HostSweepDueAt.UTC().Truncate(time.Second), + "the sweep's instant is the stored one, not the cron's") + require.NotEqual(t, on.HostSweepDueAt.Truncate(time.Minute), on.NextOccurrenceAt.Truncate(time.Minute), + "fixture: the two instants must differ, which is the whole point of reporting both") + + // Switched back on, the same due row is swept. + svc.SetDatasourceReconcileSweep(true) + enqueued, err = svc.RunDatasourceReconcile(t.Context()) + require.NoError(t, err) + require.Equal(t, 1, enqueued) +} + +// A parked source names no sweep instant either: the stored instant is cleared, +// which is what the park and restore paths use as the switch, and there is then +// no pass for this host to take. +func TestAParkedSourceNamesNoSweepInstant(t *testing.T) { + parked := &business.DatasourceSource{ + ID: "2a3f5c1e-0000-4000-8000-000000000004", OrgID: "org", + Status: "active", ReconcileInterval: 30 * time.Minute, + } + declared := business.DeclareSourceTrigger(parked, nil, nil, true) + require.False(t, declared.Declarable()) + require.True(t, declared.HostSweepEnabled, "the switch is a deployment fact, not a per-source one") + require.Nil(t, declared.HostSweepDueAt) +} + // One occurrence's enqueue key names the ORGANIZATION and the SOURCE as well as // the effect (issue #1112, m2 — and N4 on PR #1115, which found the fix // untested). diff --git a/module/services/accounts/code/pkg/business/service.go b/module/services/accounts/code/pkg/business/service.go index c5ac4a1c5..2026c9615 100644 --- a/module/services/accounts/code/pkg/business/service.go +++ b/module/services/accounts/code/pkg/business/service.go @@ -94,7 +94,9 @@ type Service struct { githubAppClientSecret string // its secret; without the pair, an installation cannot be attributed to a caller //nolint:lll // the mount this mirrors is a single deployment switch; splitting the note loses the pairing datasourceWebhookMounted bool // whether THIS deployment mounted the per-source push receiver; a stored per-source secret verifies nothing without it - datasourceTicketSigner *datasourceTicketSigner // mints/verifies opaque content tickets for oversized change-set blobs + //nolint:lll // one deployment switch; the stopgap it disables is named in datasource_deliveries.go + datasourceReconcileSweepOff bool // STOPGAP switch (#1121): true stops this host firing the reconcile schedule it DECLARES + datasourceTicketSigner *datasourceTicketSigner // mints/verifies opaque content tickets for oversized change-set blobs newGitHubClient func(token string) GitHubContentClient newAPIClient func(cfg APIDatasourceConfig, credential string) APIContentClient newCrawlerClient func(cfg CrawlerDatasourceConfig) CrawlerContentClient diff --git a/module/services/accounts/code/pkg/business/source_delegation.go b/module/services/accounts/code/pkg/business/source_delegation.go index 6841e4dc2..110629137 100644 --- a/module/services/accounts/code/pkg/business/source_delegation.go +++ b/module/services/accounts/code/pkg/business/source_delegation.go @@ -229,6 +229,22 @@ type SourceDelegationTarget struct { Binding ModuleOperationAudience } +// ExecutesDeclaredWork reports whether this target's binding is one the module +// holding it can admit as its OWN incoming execution context: its audience is +// its own declared prefix. +// +// It is what makes a target the executing module of a declared trigger rather +// than a module whose delegation some other module presents (issue #1121, N8). +// A trigger's reference arm is a source id, a mint by source id resolves the +// active delegation of the CALLING module, and the capability is addressed to +// the binding's audience — so a delegation to module A addressed to audience B +// can serve a trigger only through the delegation-id exchange, which no +// reference this host declares can reach. See selfAdmittingBinding for why +// naming your own prefix is admitted at all. +func (t SourceDelegationTarget) ExecutesDeclaredWork() bool { + return t.Binding.Audience != "" && t.Binding.Audience == t.Prefix +} + // SourceDelegationTargets lists, sorted by module prefix, the binding of every // declared module that accepts source delegations. The declaration is the // opt-in; the module's declared tenancy is deliberately not consulted, because @@ -267,9 +283,19 @@ const ( // theirs. sourceDelegationReplace sourceDelegationRecordMode = iota // sourceDelegationBackfill is a reference recorded for a source that is - // ALREADY connected (DelegateSourceTriggers). It revokes nothing: a target - // already holding an active delegation is left exactly as it is, and only a - // target with none gets one. + // ALREADY connected (DelegateSourceTriggers). It leaves a target's live + // delegation exactly as it is, and only a target with none — or with one + // its module's current declaration no longer supports — gets one. + // + // It is also narrowed to the modules that EXECUTE declared work + // (SourceDelegationTarget.ExecutesDeclaredWork), because that is what it is + // for: the reference a declared release's trigger names. It used to record + // one for every module accepting delegations, which hands the calling + // administrator's authority over that source to modules whose delegation no + // trigger of this host could ever name — authority nobody asked to grant + // (issue #1121, N8). A connect still delegates to every target: that is a + // person establishing the source, and the consuming module's sync is the + // delegation's main purpose. // // The distinction is issue #1112, M2. The backfill used to run the replace // mode, which took every delegation on the source, marked it `replaced` and @@ -312,12 +338,34 @@ func (s *Service) recordSourceDelegationsTx( recorded := 0 for _, target := range targets { if mode == sourceDelegationBackfill { + if !target.ExecutesDeclaredWork() { + continue + } existing, err := s.store.ActiveSourceDelegation(ctx, sourceID, target.Prefix) if err != nil { return recorded, fmt.Errorf("read active source delegation: %w", err) } + // A live delegation is the operator's answer that nothing was + // missing — but only while the module's current declaration still + // supports it. One recorded against a binding that has since + // changed its audience or its delegation scopes (or that is gone) + // is refused by the very next mint, which revokes it as + // `binding_changed`: leaving it in place reported "nothing was + // missing" over a source whose trigger could never mint + // (issue #1121, N8). So it is revoked here, for the same reason and + // under the same name, and the fresh one is recorded in the same + // transaction. Nothing live is re-owned — a delegation conferring + // nothing is not authority anybody holds. if existing != nil { - continue + if existing.BindingID == target.BindingID && + existing.BindingDigest == SourceDelegationBindingDigest(target.Binding) { + continue + } + if err := s.revokeSourceDelegationsTx(ctx, actorID, SourceDelegationFilter{ + OrgID: orgID, SourceID: sourceID, ModulePrefix: target.Prefix, + }, SourceDelegationBindingChanged); err != nil { + return recorded, err + } } } else { replaced, err := s.store.RevokeSourceDelegations(ctx, SourceDelegationFilter{ diff --git a/module/services/accounts/code/pkg/business/source_delegation_self_exchange_test.go b/module/services/accounts/code/pkg/business/source_delegation_self_exchange_test.go new file mode 100644 index 000000000..3e50caec4 --- /dev/null +++ b/module/services/accounts/code/pkg/business/source_delegation_self_exchange_test.go @@ -0,0 +1,106 @@ +package business + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +// The exchange when the DELEGATOR IS THE CALLER (issue #1121, m1). +// +// This is the shape unattended work runs in, and until now it was reached only +// through fixtures that happen to declare a self-audience — where a successful +// parse says nothing about why the audience link held. The link is the whole +// reason a reference is safe to hold: a delegation is a grant to one binding of +// one module, that binding declares the audience it may call, and only the +// module named as that audience may present the reference. For a module that +// executes a declared release the audience is itself — it mints the capability +// and then admits it as its own incoming execution context — so delegator and +// caller are one principal and the comparison is `prefix == prefix`. +// +// Asserted here directly, on the exchange, with the two neighbouring states +// that must still refuse: an audience naming somebody else, and a caller +// naming a binding of its own that the delegation does not authorize. +func TestADelegatorExchangesItsOwnReferenceWhenItIsTheAudience(t *testing.T) { + exchange := func(t *testing.T, audience string) (SourceOperationContextAuthority, error) { + t.Helper() + read := []ModuleOperationScope{{ResourceKind: "collections", Actions: []string{"read"}}} + write := []ModuleOperationScope{{ResourceKind: "collections", Actions: []string{"write"}}} + both := []ModuleOperationScope{{ResourceKind: "collections", Actions: []string{"read", "write"}}} + // One binding, declaring BOTH grants: the delegation scopes a person's + // connect records against, and the audience the executing module admits + // its own occurrences under. That pairing is what selfAdmittingBinding + // allows, and it is why a composition can run unattended work at all. + occurrences := ModuleOperationAudience{ + Audience: audience, InvokeScopes: both, LookupScopes: read, SourceDelegationScopes: write, + } + runtime := ModulePrincipalGrant{ + Prefix: "runtime", Tenant: testEnvelopeTenant, + OperationAudiences: map[string]ModuleOperationAudience{"occurrences": occurrences}, + } + store := &referenceExchangeStore{delegation: &SourceDelegation{ + ID: "example-delegation", OrgID: testEnvelopeTenant, PrincipalID: "example-person", + SourceID: "example-source", ModulePrefix: "runtime", BindingID: "occurrences", + BindingDigest: SourceDelegationBindingDigest(occurrences), + }} + s := &Service{store: store} + s.SetModuleAuthorityReads(reviewLiveAuthority{service: s}, nil) + s.SetModuleEnvelopeStore(&fakeEnvelopeStore{latest: 1}) + s.InstallDerivedModulePrincipals(ModulePrincipalRegistry{ModulePrincipalID("runtime"): runtime}, 1, nil) + caller := ModuleCaller{PrincipalID: ModulePrincipalID("runtime"), BoundOrg: testEnvelopeTenant} + return s.AuthorizeDelegationReferenceExchange(context.Background(), caller, store.delegation.ID, "occurrences", false) + } + + authority, err := exchange(t, "runtime") + require.NoError(t, err, "the module that executes the release is the audience its own delegation names") + // The capability is the person's, used by the module: exactly what the + // parent-token arm produces, so no consumer can tell which arm admitted it. + require.Equal(t, "example-person", authority.OwnerPrincipalID) + require.Equal(t, ModulePrincipalID("runtime"), authority.PrincipalID) + require.Equal(t, "runtime", authority.ModulePrefix) + require.Equal(t, "runtime", authority.Audience) + require.Equal(t, testEnvelopeTenant, authority.Tenant, "the tenant is the delegation's organization, never the caller's request") + require.Equal(t, "example-delegation", authority.Delegation.ID) + // What the person delegated, and what this one call may do with it: the + // binding's delegation scopes, and the caller's invoke reach narrowed to + // them. Write and not read: a self-exchange is not a wider grant. + require.Equal(t, []ModuleOperationScope{{ResourceKind: "collections", Actions: []string{"write"}}}, authority.Scopes) + require.Equal(t, []ModuleOperationScope{{ResourceKind: "collections", Actions: []string{"write"}}}, authority.ActorScopes) + + // The same registry with the audience naming somebody else: the caller is + // no longer the audience of the delegation it holds, and holding the + // delegation is not what authorizes presenting it. + _, err = exchange(t, "somebody-else") + require.ErrorIs(t, err, ErrSourceDelegationInvalid, + "a module that is not the audience cannot exchange its own delegation") +} + +// A caller that is the audience but names a binding of its own the delegation +// does not authorize is refused: the binding half of the link is checked +// separately from the audience half. +func TestASelfExchangeStillNeedsTheBindingItNames(t *testing.T) { + read := []ModuleOperationScope{{ResourceKind: "collections", Actions: []string{"read"}}} + write := []ModuleOperationScope{{ResourceKind: "collections", Actions: []string{"write"}}} + both := []ModuleOperationScope{{ResourceKind: "collections", Actions: []string{"read", "write"}}} + occurrences := ModuleOperationAudience{ + Audience: "runtime", InvokeScopes: both, LookupScopes: read, SourceDelegationScopes: write, + } + runtime := ModulePrincipalGrant{ + Prefix: "runtime", Tenant: testEnvelopeTenant, + OperationAudiences: map[string]ModuleOperationAudience{"occurrences": occurrences}, + } + store := &referenceExchangeStore{delegation: &SourceDelegation{ + ID: "example-delegation", OrgID: testEnvelopeTenant, PrincipalID: "example-person", + SourceID: "example-source", ModulePrefix: "runtime", BindingID: "occurrences", + BindingDigest: SourceDelegationBindingDigest(occurrences), + }} + s := &Service{store: store} + s.SetModuleAuthorityReads(reviewLiveAuthority{service: s}, nil) + s.SetModuleEnvelopeStore(&fakeEnvelopeStore{latest: 1}) + s.InstallDerivedModulePrincipals(ModulePrincipalRegistry{ModulePrincipalID("runtime"): runtime}, 1, nil) + caller := ModuleCaller{PrincipalID: ModulePrincipalID("runtime"), BoundOrg: testEnvelopeTenant} + + _, err := s.AuthorizeDelegationReferenceExchange(context.Background(), caller, store.delegation.ID, "not-declared", false) + require.ErrorIs(t, err, ErrSourceDelegationInvalid, "a binding the module does not declare authorizes nothing") +} diff --git a/module/services/accounts/code/pkg/business/source_operation_receipts.go b/module/services/accounts/code/pkg/business/source_operation_receipts.go index 90141879c..9215dc736 100644 --- a/module/services/accounts/code/pkg/business/source_operation_receipts.go +++ b/module/services/accounts/code/pkg/business/source_operation_receipts.go @@ -77,6 +77,35 @@ func (s *Service) SourceOperationAttemptForActor(ctx context.Context, actor, org return attempt, err } +// unattendedPassKey carries the delegation a source operation was admitted +// under, when one admitted it. +// +// It exists for the audit trail (issue #1121, m3). A delegated pass +// authenticates as the DELEGATING PERSON — that is the reference arm's whole +// design, and ADR 0008 puts that person and their chain in the trail +// deliberately — so the actor is a `user` and is not a mis-attribution. What +// the row could not say is that nobody was present for it: the two passes stamp +// an identical request identity, and business cannot tell them apart from +// anything else on the context. The adapter can: it has just confirmed the +// delegation. So it says so, here, and the observation records it. +type unattendedPassKey struct{} + +// WithUnattendedPass marks this request as a pass admitted by the delegation +// `delegationID`, with no person present. Called by the authority layer that +// confirmed the delegation, never from a request field. +func WithUnattendedPass(ctx context.Context, delegationID string) context.Context { + if delegationID == "" { + return ctx + } + return context.WithValue(ctx, unattendedPassKey{}, delegationID) +} + +// unattendedPass reports the delegation this pass was admitted under, if any. +func unattendedPass(ctx context.Context) (string, bool) { + id, ok := ctx.Value(unattendedPassKey{}).(string) + return id, ok && id != "" +} + // A request may wait behind another holder of its effect id. Recheck authority // after that wait, immediately before the SDK reads or replays a receipt. type sourceOperationRecheckKey struct{} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/datasource.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/datasource.pb.go index c7a189207..d7d1e6868 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/datasource.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/datasource.pb.go @@ -5496,6 +5496,16 @@ func (x *RevokeSourceDelegationResponse) GetDelegation() *SourceDelegation { // the capability that admitted it and revoking the delegation refuses the // next occurrence. // +// IT IS SET ONLY FOR A DELEGATION THE EXECUTING MODULE CAN USE. A mint by +// source id resolves the active delegation of the CALLING module, and the +// capability is addressed to the binding's declared audience, which the +// executing module must itself admit — so the one shape a declared +// reference can serve is a delegation whose binding names its own module +// as the audience. A delegation held by one module and addressed to +// another is reachable only through the delegation-id exchange, which no +// reference this host declares can name, and declaring it would read as +// bound while every occurrence was refused at the mint. +// // IT IS NOT OPAQUE, and revocation is therefore not permanent. The value // is the source's own id, and a mint resolves whichever delegation of that // source is active, so revoking one and then RECONNECTING the source @@ -5511,11 +5521,15 @@ func (x *RevokeSourceDelegationResponse) GetDelegation() *SourceDelegation { // binding's headless scopes — an ordinary token admission, bounded by the // capability's own short lifetime. // -// IT IS PLANNED, and no declaration this host builds has ever set it. It -// cannot serve the reconcile trigger: the headless mint lives 60 seconds -// against that trigger's 10-minute declared deadline, and the capability -// is the module's own principal while ReconcileSource requires an -// organization member. +// IT IS PLANNED, and no declaration this host builds has ever set it, so +// nothing anywhere has proved it against a trigger. It cannot serve the +// reconcile trigger: the headless mint lives 60 seconds against that +// trigger's 10-minute declared deadline, and the capability is the +// module's own principal while ReconcileSource requires an organization +// member. That mint IS the per-tenant module identity a module publishing +// for one tenant presents (PlatformAdminService.ResolveModuleTenantIdentity +// reads what it would assert); what it is not is an identity for this +// trigger's occurrences. // // THE NAMES ARE THE INSTALLATION DOCUMENT'S, not this host's and not the // executing module's read API's. That distinction is load-bearing: the module @@ -5969,8 +5983,24 @@ type SourceTriggerDeclaration struct { // schedule is disabled, or its interval is not one a five-field cron can // express. Empty means the declaration above is complete. UndeclarableReason string `protobuf:"bytes,8,opt,name=undeclarable_reason,json=undeclarableReason,proto3" json:"undeclarable_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // host_sweep_enabled says whether THIS HOST still fires the schedule it + // declares, as a labelled stopgap for the absence of a module that consumes + // the declaration, and host_sweep_due_at the instant it will next sweep on. + // + // THAT INSTANT IS NOT next_occurrence_at, and the two are reported + // separately because they share a period and not a phase. The declared cron + // for a sub-hourly interval is `*/M * * * *`, which comes due on the quarter + // or half hour; the sweep fires on the source's stored instant, which every + // pass moves to `now + interval` — an arbitrary offset inside the period. A + // reader shown only the cron's instant was reading what the declaration + // SAYS while the pass that will actually happen was at another minute. + // + // host_sweep_due_at is unset when the sweep is off, or when the source is + // held off its schedule: then there is no pass this host will take. + HostSweepEnabled bool `protobuf:"varint,9,opt,name=host_sweep_enabled,json=hostSweepEnabled,proto3" json:"host_sweep_enabled,omitempty"` + HostSweepDueAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=host_sweep_due_at,json=hostSweepDueAt,proto3" json:"host_sweep_due_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SourceTriggerDeclaration) Reset() { @@ -6059,6 +6089,20 @@ func (x *SourceTriggerDeclaration) GetUndeclarableReason() string { return "" } +func (x *SourceTriggerDeclaration) GetHostSweepEnabled() bool { + if x != nil { + return x.HostSweepEnabled + } + return false +} + +func (x *SourceTriggerDeclaration) GetHostSweepDueAt() *timestamppb.Timestamp { + if x != nil { + return x.HostSweepDueAt + } + return nil +} + type DelegateSourceTriggersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -7586,7 +7630,7 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "\x06scopes\x18\x05 \x03(\tR\x06scopes\x12\x16\n" + "\x06lookup\x18\x06 \x01(\bR\x06lookup\x127\n" + "\tminted_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\bmintedAt\x12\x16\n" + - "\x06record\x18\b \x01(\tR\x06record\"\xb5\x03\n" + + "\x06record\x18\b \x01(\tR\x06record\"\xaa\x04\n" + "\x18SourceTriggerDeclaration\x12\x18\n" + "\arelease\x18\x01 \x01(\tR\arelease\x12\x1c\n" + "\toperation\x18\x02 \x01(\tR\toperation\x12E\n" + @@ -7595,7 +7639,10 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "\x06digest\x18\x05 \x01(\tR\x06digest\x12H\n" + "\x12last_occurrence_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\x10lastOccurrenceAt\x12H\n" + "\x12next_occurrence_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\x10nextOccurrenceAt\x12/\n" + - "\x13undeclarable_reason\x18\b \x01(\tR\x12undeclarableReason\"g\n" + + "\x13undeclarable_reason\x18\b \x01(\tR\x12undeclarableReason\x12,\n" + + "\x12host_sweep_enabled\x18\t \x01(\bR\x10hostSweepEnabled\x12E\n" + + "\x11host_sweep_due_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\x0ehostSweepDueAt\"g\n" + "\x1dDelegateSourceTriggersRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12%\n" + "\tsource_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\bsourceId\"p\n" + @@ -8062,88 +8109,89 @@ var file_saas_accounts_v1_datasource_proto_depIdxs = []int32{ 79, // 76: saas.accounts.v1.SourceTriggerDeclaration.identity:type_name -> saas.accounts.v1.TriggerIdentity 103, // 77: saas.accounts.v1.SourceTriggerDeclaration.last_occurrence_at:type_name -> google.protobuf.Timestamp 103, // 78: saas.accounts.v1.SourceTriggerDeclaration.next_occurrence_at:type_name -> google.protobuf.Timestamp - 81, // 79: saas.accounts.v1.DelegateSourceTriggersResponse.delegations:type_name -> saas.accounts.v1.TriggerDelegationReference - 83, // 80: saas.accounts.v1.GetSourceTriggerDeclarationResponse.declaration:type_name -> saas.accounts.v1.SourceTriggerDeclaration - 81, // 81: saas.accounts.v1.GetSourceTriggerDeclarationResponse.delegations:type_name -> saas.accounts.v1.TriggerDelegationReference - 82, // 82: saas.accounts.v1.GetSourceTriggerDeclarationResponse.children:type_name -> saas.accounts.v1.TriggerChildContext - 107, // 83: saas.accounts.v1.SourceOperation.input_schema:type_name -> google.protobuf.Struct - 107, // 84: saas.accounts.v1.SourceOperation.output_schema:type_name -> google.protobuf.Struct - 13, // 85: saas.accounts.v1.SourceOperation.effect:type_name -> saas.accounts.v1.SourceOperation.Effect - 91, // 86: saas.accounts.v1.DeclareSourceOperationsRequest.operations:type_name -> saas.accounts.v1.SourceOperation - 91, // 87: saas.accounts.v1.DeclareSourceOperationsResponse.operations:type_name -> saas.accounts.v1.SourceOperation - 91, // 88: saas.accounts.v1.ListSourceOperationsResponse.operations:type_name -> saas.accounts.v1.SourceOperation - 97, // 89: saas.accounts.v1.InvokeSourceOperationResponse.receipt:type_name -> saas.accounts.v1.SourceOperationReceipt - 100, // 90: saas.accounts.v1.DatasourceService.PruneSourceOperationReceipts:input_type -> saas.accounts.v1.PruneSourceOperationReceiptsRequest - 102, // 91: saas.accounts.v1.DatasourceService.LookupPruneSourceOperationReceipts:input_type -> saas.accounts.v1.LookupPruneSourceOperationReceiptsRequest - 96, // 92: saas.accounts.v1.DatasourceService.InvokeSourceOperation:input_type -> saas.accounts.v1.InvokeSourceOperationRequest - 99, // 93: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:input_type -> saas.accounts.v1.LookupInvokeSourceOperationRequest - 94, // 94: saas.accounts.v1.DatasourceService.ListSourceOperations:input_type -> saas.accounts.v1.ListSourceOperationsRequest - 92, // 95: saas.accounts.v1.DatasourceService.DeclareSourceOperations:input_type -> saas.accounts.v1.DeclareSourceOperationsRequest - 20, // 96: saas.accounts.v1.DatasourceService.AddGitHubSource:input_type -> saas.accounts.v1.AddGitHubSourceRequest - 22, // 97: saas.accounts.v1.DatasourceService.AddSource:input_type -> saas.accounts.v1.AddSourceRequest - 27, // 98: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:input_type -> saas.accounts.v1.GetDatasourceCatalogRequest - 29, // 99: saas.accounts.v1.DatasourceService.ListSources:input_type -> saas.accounts.v1.ListSourcesRequest - 31, // 100: saas.accounts.v1.DatasourceService.GetSource:input_type -> saas.accounts.v1.GetSourceRequest - 33, // 101: saas.accounts.v1.DatasourceService.SyncSource:input_type -> saas.accounts.v1.SyncSourceRequest - 35, // 102: saas.accounts.v1.DatasourceService.GetSourceSync:input_type -> saas.accounts.v1.GetSourceSyncRequest - 41, // 103: saas.accounts.v1.DatasourceService.DeleteSource:input_type -> saas.accounts.v1.DeleteSourceRequest - 44, // 104: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:input_type -> saas.accounts.v1.BeginGitHubAppSetupRequest - 46, // 105: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:input_type -> saas.accounts.v1.CompleteGitHubAppSetupRequest - 48, // 106: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:input_type -> saas.accounts.v1.MigrateGitHubSourceToAppRequest - 54, // 107: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:input_type -> saas.accounts.v1.BeginDatasourceAccountLinkRequest - 56, // 108: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:input_type -> saas.accounts.v1.CompleteDatasourceAccountLinkRequest - 58, // 109: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:input_type -> saas.accounts.v1.ListMyDatasourceAccountLinksRequest - 60, // 110: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:input_type -> saas.accounts.v1.DeleteDatasourceAccountLinkRequest - 62, // 111: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:input_type -> saas.accounts.v1.GetDatasourceDirectoryRequest - 64, // 112: saas.accounts.v1.DatasourceService.BindDatasourceGroup:input_type -> saas.accounts.v1.BindDatasourceGroupRequest - 66, // 113: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:input_type -> saas.accounts.v1.UnbindDatasourceGroupRequest - 68, // 114: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:input_type -> saas.accounts.v1.ClaimDatasourceDomainRequest - 70, // 115: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:input_type -> saas.accounts.v1.VerifyDatasourceDomainRequest - 72, // 116: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:input_type -> saas.accounts.v1.DeleteDatasourceDomainRequest - 75, // 117: saas.accounts.v1.DatasourceService.ListSourceDelegations:input_type -> saas.accounts.v1.ListSourceDelegationsRequest - 77, // 118: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:input_type -> saas.accounts.v1.RevokeSourceDelegationRequest - 84, // 119: saas.accounts.v1.DatasourceService.DelegateSourceTriggers:input_type -> saas.accounts.v1.DelegateSourceTriggersRequest - 86, // 120: saas.accounts.v1.DatasourceService.GetSourceTriggerDeclaration:input_type -> saas.accounts.v1.GetSourceTriggerDeclarationRequest - 88, // 121: saas.accounts.v1.DatasourceService.ReconcileSource:input_type -> saas.accounts.v1.ReconcileSourceRequest - 90, // 122: saas.accounts.v1.DatasourceService.LookupReconcileSource:input_type -> saas.accounts.v1.LookupReconcileSourceRequest - 101, // 123: saas.accounts.v1.DatasourceService.PruneSourceOperationReceipts:output_type -> saas.accounts.v1.PruneSourceOperationReceiptsResponse - 101, // 124: saas.accounts.v1.DatasourceService.LookupPruneSourceOperationReceipts:output_type -> saas.accounts.v1.PruneSourceOperationReceiptsResponse - 98, // 125: saas.accounts.v1.DatasourceService.InvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse - 98, // 126: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse - 95, // 127: saas.accounts.v1.DatasourceService.ListSourceOperations:output_type -> saas.accounts.v1.ListSourceOperationsResponse - 93, // 128: saas.accounts.v1.DatasourceService.DeclareSourceOperations:output_type -> saas.accounts.v1.DeclareSourceOperationsResponse - 21, // 129: saas.accounts.v1.DatasourceService.AddGitHubSource:output_type -> saas.accounts.v1.AddGitHubSourceResponse - 23, // 130: saas.accounts.v1.DatasourceService.AddSource:output_type -> saas.accounts.v1.AddSourceResponse - 28, // 131: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:output_type -> saas.accounts.v1.GetDatasourceCatalogResponse - 30, // 132: saas.accounts.v1.DatasourceService.ListSources:output_type -> saas.accounts.v1.ListSourcesResponse - 32, // 133: saas.accounts.v1.DatasourceService.GetSource:output_type -> saas.accounts.v1.GetSourceResponse - 34, // 134: saas.accounts.v1.DatasourceService.SyncSource:output_type -> saas.accounts.v1.SyncSourceResponse - 40, // 135: saas.accounts.v1.DatasourceService.GetSourceSync:output_type -> saas.accounts.v1.GetSourceSyncResponse - 42, // 136: saas.accounts.v1.DatasourceService.DeleteSource:output_type -> saas.accounts.v1.DeleteSourceResponse - 45, // 137: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:output_type -> saas.accounts.v1.BeginGitHubAppSetupResponse - 47, // 138: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:output_type -> saas.accounts.v1.CompleteGitHubAppSetupResponse - 49, // 139: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:output_type -> saas.accounts.v1.MigrateGitHubSourceToAppResponse - 55, // 140: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:output_type -> saas.accounts.v1.BeginDatasourceAccountLinkResponse - 57, // 141: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:output_type -> saas.accounts.v1.CompleteDatasourceAccountLinkResponse - 59, // 142: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:output_type -> saas.accounts.v1.ListMyDatasourceAccountLinksResponse - 61, // 143: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:output_type -> saas.accounts.v1.DeleteDatasourceAccountLinkResponse - 63, // 144: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:output_type -> saas.accounts.v1.GetDatasourceDirectoryResponse - 65, // 145: saas.accounts.v1.DatasourceService.BindDatasourceGroup:output_type -> saas.accounts.v1.BindDatasourceGroupResponse - 67, // 146: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:output_type -> saas.accounts.v1.UnbindDatasourceGroupResponse - 69, // 147: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:output_type -> saas.accounts.v1.ClaimDatasourceDomainResponse - 71, // 148: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:output_type -> saas.accounts.v1.VerifyDatasourceDomainResponse - 73, // 149: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:output_type -> saas.accounts.v1.DeleteDatasourceDomainResponse - 76, // 150: saas.accounts.v1.DatasourceService.ListSourceDelegations:output_type -> saas.accounts.v1.ListSourceDelegationsResponse - 78, // 151: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:output_type -> saas.accounts.v1.RevokeSourceDelegationResponse - 85, // 152: saas.accounts.v1.DatasourceService.DelegateSourceTriggers:output_type -> saas.accounts.v1.DelegateSourceTriggersResponse - 87, // 153: saas.accounts.v1.DatasourceService.GetSourceTriggerDeclaration:output_type -> saas.accounts.v1.GetSourceTriggerDeclarationResponse - 89, // 154: saas.accounts.v1.DatasourceService.ReconcileSource:output_type -> saas.accounts.v1.ReconcileSourceResponse - 89, // 155: saas.accounts.v1.DatasourceService.LookupReconcileSource:output_type -> saas.accounts.v1.ReconcileSourceResponse - 123, // [123:156] is the sub-list for method output_type - 90, // [90:123] is the sub-list for method input_type - 90, // [90:90] is the sub-list for extension type_name - 90, // [90:90] is the sub-list for extension extendee - 0, // [0:90] is the sub-list for field type_name + 103, // 79: saas.accounts.v1.SourceTriggerDeclaration.host_sweep_due_at:type_name -> google.protobuf.Timestamp + 81, // 80: saas.accounts.v1.DelegateSourceTriggersResponse.delegations:type_name -> saas.accounts.v1.TriggerDelegationReference + 83, // 81: saas.accounts.v1.GetSourceTriggerDeclarationResponse.declaration:type_name -> saas.accounts.v1.SourceTriggerDeclaration + 81, // 82: saas.accounts.v1.GetSourceTriggerDeclarationResponse.delegations:type_name -> saas.accounts.v1.TriggerDelegationReference + 82, // 83: saas.accounts.v1.GetSourceTriggerDeclarationResponse.children:type_name -> saas.accounts.v1.TriggerChildContext + 107, // 84: saas.accounts.v1.SourceOperation.input_schema:type_name -> google.protobuf.Struct + 107, // 85: saas.accounts.v1.SourceOperation.output_schema:type_name -> google.protobuf.Struct + 13, // 86: saas.accounts.v1.SourceOperation.effect:type_name -> saas.accounts.v1.SourceOperation.Effect + 91, // 87: saas.accounts.v1.DeclareSourceOperationsRequest.operations:type_name -> saas.accounts.v1.SourceOperation + 91, // 88: saas.accounts.v1.DeclareSourceOperationsResponse.operations:type_name -> saas.accounts.v1.SourceOperation + 91, // 89: saas.accounts.v1.ListSourceOperationsResponse.operations:type_name -> saas.accounts.v1.SourceOperation + 97, // 90: saas.accounts.v1.InvokeSourceOperationResponse.receipt:type_name -> saas.accounts.v1.SourceOperationReceipt + 100, // 91: saas.accounts.v1.DatasourceService.PruneSourceOperationReceipts:input_type -> saas.accounts.v1.PruneSourceOperationReceiptsRequest + 102, // 92: saas.accounts.v1.DatasourceService.LookupPruneSourceOperationReceipts:input_type -> saas.accounts.v1.LookupPruneSourceOperationReceiptsRequest + 96, // 93: saas.accounts.v1.DatasourceService.InvokeSourceOperation:input_type -> saas.accounts.v1.InvokeSourceOperationRequest + 99, // 94: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:input_type -> saas.accounts.v1.LookupInvokeSourceOperationRequest + 94, // 95: saas.accounts.v1.DatasourceService.ListSourceOperations:input_type -> saas.accounts.v1.ListSourceOperationsRequest + 92, // 96: saas.accounts.v1.DatasourceService.DeclareSourceOperations:input_type -> saas.accounts.v1.DeclareSourceOperationsRequest + 20, // 97: saas.accounts.v1.DatasourceService.AddGitHubSource:input_type -> saas.accounts.v1.AddGitHubSourceRequest + 22, // 98: saas.accounts.v1.DatasourceService.AddSource:input_type -> saas.accounts.v1.AddSourceRequest + 27, // 99: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:input_type -> saas.accounts.v1.GetDatasourceCatalogRequest + 29, // 100: saas.accounts.v1.DatasourceService.ListSources:input_type -> saas.accounts.v1.ListSourcesRequest + 31, // 101: saas.accounts.v1.DatasourceService.GetSource:input_type -> saas.accounts.v1.GetSourceRequest + 33, // 102: saas.accounts.v1.DatasourceService.SyncSource:input_type -> saas.accounts.v1.SyncSourceRequest + 35, // 103: saas.accounts.v1.DatasourceService.GetSourceSync:input_type -> saas.accounts.v1.GetSourceSyncRequest + 41, // 104: saas.accounts.v1.DatasourceService.DeleteSource:input_type -> saas.accounts.v1.DeleteSourceRequest + 44, // 105: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:input_type -> saas.accounts.v1.BeginGitHubAppSetupRequest + 46, // 106: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:input_type -> saas.accounts.v1.CompleteGitHubAppSetupRequest + 48, // 107: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:input_type -> saas.accounts.v1.MigrateGitHubSourceToAppRequest + 54, // 108: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:input_type -> saas.accounts.v1.BeginDatasourceAccountLinkRequest + 56, // 109: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:input_type -> saas.accounts.v1.CompleteDatasourceAccountLinkRequest + 58, // 110: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:input_type -> saas.accounts.v1.ListMyDatasourceAccountLinksRequest + 60, // 111: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:input_type -> saas.accounts.v1.DeleteDatasourceAccountLinkRequest + 62, // 112: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:input_type -> saas.accounts.v1.GetDatasourceDirectoryRequest + 64, // 113: saas.accounts.v1.DatasourceService.BindDatasourceGroup:input_type -> saas.accounts.v1.BindDatasourceGroupRequest + 66, // 114: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:input_type -> saas.accounts.v1.UnbindDatasourceGroupRequest + 68, // 115: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:input_type -> saas.accounts.v1.ClaimDatasourceDomainRequest + 70, // 116: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:input_type -> saas.accounts.v1.VerifyDatasourceDomainRequest + 72, // 117: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:input_type -> saas.accounts.v1.DeleteDatasourceDomainRequest + 75, // 118: saas.accounts.v1.DatasourceService.ListSourceDelegations:input_type -> saas.accounts.v1.ListSourceDelegationsRequest + 77, // 119: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:input_type -> saas.accounts.v1.RevokeSourceDelegationRequest + 84, // 120: saas.accounts.v1.DatasourceService.DelegateSourceTriggers:input_type -> saas.accounts.v1.DelegateSourceTriggersRequest + 86, // 121: saas.accounts.v1.DatasourceService.GetSourceTriggerDeclaration:input_type -> saas.accounts.v1.GetSourceTriggerDeclarationRequest + 88, // 122: saas.accounts.v1.DatasourceService.ReconcileSource:input_type -> saas.accounts.v1.ReconcileSourceRequest + 90, // 123: saas.accounts.v1.DatasourceService.LookupReconcileSource:input_type -> saas.accounts.v1.LookupReconcileSourceRequest + 101, // 124: saas.accounts.v1.DatasourceService.PruneSourceOperationReceipts:output_type -> saas.accounts.v1.PruneSourceOperationReceiptsResponse + 101, // 125: saas.accounts.v1.DatasourceService.LookupPruneSourceOperationReceipts:output_type -> saas.accounts.v1.PruneSourceOperationReceiptsResponse + 98, // 126: saas.accounts.v1.DatasourceService.InvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse + 98, // 127: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse + 95, // 128: saas.accounts.v1.DatasourceService.ListSourceOperations:output_type -> saas.accounts.v1.ListSourceOperationsResponse + 93, // 129: saas.accounts.v1.DatasourceService.DeclareSourceOperations:output_type -> saas.accounts.v1.DeclareSourceOperationsResponse + 21, // 130: saas.accounts.v1.DatasourceService.AddGitHubSource:output_type -> saas.accounts.v1.AddGitHubSourceResponse + 23, // 131: saas.accounts.v1.DatasourceService.AddSource:output_type -> saas.accounts.v1.AddSourceResponse + 28, // 132: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:output_type -> saas.accounts.v1.GetDatasourceCatalogResponse + 30, // 133: saas.accounts.v1.DatasourceService.ListSources:output_type -> saas.accounts.v1.ListSourcesResponse + 32, // 134: saas.accounts.v1.DatasourceService.GetSource:output_type -> saas.accounts.v1.GetSourceResponse + 34, // 135: saas.accounts.v1.DatasourceService.SyncSource:output_type -> saas.accounts.v1.SyncSourceResponse + 40, // 136: saas.accounts.v1.DatasourceService.GetSourceSync:output_type -> saas.accounts.v1.GetSourceSyncResponse + 42, // 137: saas.accounts.v1.DatasourceService.DeleteSource:output_type -> saas.accounts.v1.DeleteSourceResponse + 45, // 138: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:output_type -> saas.accounts.v1.BeginGitHubAppSetupResponse + 47, // 139: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:output_type -> saas.accounts.v1.CompleteGitHubAppSetupResponse + 49, // 140: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:output_type -> saas.accounts.v1.MigrateGitHubSourceToAppResponse + 55, // 141: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:output_type -> saas.accounts.v1.BeginDatasourceAccountLinkResponse + 57, // 142: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:output_type -> saas.accounts.v1.CompleteDatasourceAccountLinkResponse + 59, // 143: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:output_type -> saas.accounts.v1.ListMyDatasourceAccountLinksResponse + 61, // 144: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:output_type -> saas.accounts.v1.DeleteDatasourceAccountLinkResponse + 63, // 145: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:output_type -> saas.accounts.v1.GetDatasourceDirectoryResponse + 65, // 146: saas.accounts.v1.DatasourceService.BindDatasourceGroup:output_type -> saas.accounts.v1.BindDatasourceGroupResponse + 67, // 147: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:output_type -> saas.accounts.v1.UnbindDatasourceGroupResponse + 69, // 148: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:output_type -> saas.accounts.v1.ClaimDatasourceDomainResponse + 71, // 149: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:output_type -> saas.accounts.v1.VerifyDatasourceDomainResponse + 73, // 150: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:output_type -> saas.accounts.v1.DeleteDatasourceDomainResponse + 76, // 151: saas.accounts.v1.DatasourceService.ListSourceDelegations:output_type -> saas.accounts.v1.ListSourceDelegationsResponse + 78, // 152: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:output_type -> saas.accounts.v1.RevokeSourceDelegationResponse + 85, // 153: saas.accounts.v1.DatasourceService.DelegateSourceTriggers:output_type -> saas.accounts.v1.DelegateSourceTriggersResponse + 87, // 154: saas.accounts.v1.DatasourceService.GetSourceTriggerDeclaration:output_type -> saas.accounts.v1.GetSourceTriggerDeclarationResponse + 89, // 155: saas.accounts.v1.DatasourceService.ReconcileSource:output_type -> saas.accounts.v1.ReconcileSourceResponse + 89, // 156: saas.accounts.v1.DatasourceService.LookupReconcileSource:output_type -> saas.accounts.v1.ReconcileSourceResponse + 124, // [124:157] is the sub-list for method output_type + 91, // [91:124] is the sub-list for method input_type + 91, // [91:91] is the sub-list for extension type_name + 91, // [91:91] is the sub-list for extension extendee + 0, // [0:91] is the sub-list for field type_name } func init() { file_saas_accounts_v1_datasource_proto_init() } diff --git a/module/services/accounts/code/work.go b/module/services/accounts/code/work.go index 9a5ff75b4..20ac4a1a1 100644 --- a/module/services/accounts/code/work.go +++ b/module/services/accounts/code/work.go @@ -1143,6 +1143,17 @@ func doWork(ctx context.Context) (Clean, error) { perSourceWebhookMounted := strings.EqualFold( strings.TrimSpace(workspaceEnv("github-app", "DATASOURCE_GITHUB_WEBHOOK_ENABLED")), "true") service.SetDatasourceWebhookReceiverMounted(perSourceWebhookMounted) + + // Whether this host still fires the reconcile schedule it DECLARES, which + // is a labelled stopgap (business/datasource_deliveries.go) and the one + // switch a composition turns off the moment a module consumes the + // declaration — the two must never both run (issue #1121, N7). A declared + // key of the application group rather than an ambient variable, and ON + // unless it says `false`: that is what every deployment does today, and a + // default that silently stopped every scheduled reconcile is the outage + // #1090 already caused once. + service.SetDatasourceReconcileSweep(!strings.EqualFold( + strings.TrimSpace(workspaceEnv("application", "DATASOURCE_RECONCILE_SWEEP_ENABLED")), "false")) if perSourceWebhookMounted { adapters.RegisterHTTPRoute(datasource.GitHubWebhookPath, datasource.NewHandler( datasource.GitHubWebhookPath, @@ -1232,6 +1243,11 @@ func doWork(ctx context.Context) (Clean, error) { // occurrence of the declared trigger admits // DatasourceService/ReconcileSource, which reaches exactly the engines // this sweep reaches. + // Off when the operator says something else fires it. The switch is + // read once here and told to the service, which refuses the sweep too + // (SetDatasourceReconcileSweep): the tick and the sweep are one fact, + // so a host that only dropped the tick would still sweep from any + // other caller and would still describe a sweep that is not running. sweepReconcile := func() { if n, err := service.RunDatasourceReconcile(retentionCtx); err != nil { rw.Warn("datasource reconcile sweep failed", wool.ErrField(err)) diff --git a/module/services/accounts/proto/saas/accounts/v1/datasource.proto b/module/services/accounts/proto/saas/accounts/v1/datasource.proto index 5b952cf9d..058fbcac2 100644 --- a/module/services/accounts/proto/saas/accounts/v1/datasource.proto +++ b/module/services/accounts/proto/saas/accounts/v1/datasource.proto @@ -1034,6 +1034,16 @@ message RevokeSourceDelegationResponse { // the capability that admitted it and revoking the delegation refuses the // next occurrence. // +// IT IS SET ONLY FOR A DELEGATION THE EXECUTING MODULE CAN USE. A mint by +// source id resolves the active delegation of the CALLING module, and the +// capability is addressed to the binding's declared audience, which the +// executing module must itself admit — so the one shape a declared +// reference can serve is a delegation whose binding names its own module +// as the audience. A delegation held by one module and addressed to +// another is reachable only through the delegation-id exchange, which no +// reference this host declares can name, and declaring it would read as +// bound while every occurrence was refused at the mint. +// // IT IS NOT OPAQUE, and revocation is therefore not permanent. The value // is the source's own id, and a mint resolves whichever delegation of that // source is active, so revoking one and then RECONNECTING the source @@ -1048,11 +1058,15 @@ message RevokeSourceDelegationResponse { // binding's headless scopes — an ordinary token admission, bounded by the // capability's own short lifetime. // -// IT IS PLANNED, and no declaration this host builds has ever set it. It -// cannot serve the reconcile trigger: the headless mint lives 60 seconds -// against that trigger's 10-minute declared deadline, and the capability -// is the module's own principal while ReconcileSource requires an -// organization member. +// IT IS PLANNED, and no declaration this host builds has ever set it, so +// nothing anywhere has proved it against a trigger. It cannot serve the +// reconcile trigger: the headless mint lives 60 seconds against that +// trigger's 10-minute declared deadline, and the capability is the +// module's own principal while ReconcileSource requires an organization +// member. That mint IS the per-tenant module identity a module publishing +// for one tenant presents (PlatformAdminService.ResolveModuleTenantIdentity +// reads what it would assert); what it is not is an identity for this +// trigger's occurrences. // // THE NAMES ARE THE INSTALLATION DOCUMENT'S, not this host's and not the // executing module's read API's. That distinction is load-bearing: the module @@ -1177,6 +1191,22 @@ message SourceTriggerDeclaration { // schedule is disabled, or its interval is not one a five-field cron can // express. Empty means the declaration above is complete. string undeclarable_reason = 8; + // host_sweep_enabled says whether THIS HOST still fires the schedule it + // declares, as a labelled stopgap for the absence of a module that consumes + // the declaration, and host_sweep_due_at the instant it will next sweep on. + // + // THAT INSTANT IS NOT next_occurrence_at, and the two are reported + // separately because they share a period and not a phase. The declared cron + // for a sub-hourly interval is `*/M * * * *`, which comes due on the quarter + // or half hour; the sweep fires on the source's stored instant, which every + // pass moves to `now + interval` — an arbitrary offset inside the period. A + // reader shown only the cron's instant was reading what the declaration + // SAYS while the pass that will actually happen was at another minute. + // + // host_sweep_due_at is unset when the sweep is off, or when the source is + // held off its schedule: then there is no pass this host will take. + bool host_sweep_enabled = 9; + google.protobuf.Timestamp host_sweep_due_at = 10; } message DelegateSourceTriggersRequest { diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/datasource_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/datasource_pb.ts index 90ea2ca4b..c124aa793 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/datasource_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/datasource_pb.ts @@ -18,7 +18,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/datasource.proto. */ export const file_saas_accounts_v1_datasource: GenFile = /*@__PURE__*/ - fileDesc("CiFzYWFzL2FjY291bnRzL3YxL2RhdGFzb3VyY2UucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoWR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIMCgRyZXBvGAEgASgJEg0KBXBhdGhzGAIgAygJEg4KBmJyYW5jaBgDIAEoCRIXCg9maWxlX2V4dGVuc2lvbnMYBCADKAki9AEKD0FwaU9BdXRoMkNvbmZpZxI2CgVncmFudBgEIAEoDjInLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnLkdyYW50EhEKCXRva2VuX3VybBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSDgoGc2NvcGVzGAMgAygJInMKBUdyYW50EhUKEUdSQU5UX1VOU1BFQ0lGSUVEEAASFwoTR1JBTlRfUkVGUkVTSF9UT0tFThABEhwKGEdSQU5UX0NMSUVOVF9DUkVERU5USUFMUxACEhwKGEdSQU5UX0FVVEhPUklaQVRJT05fQ09ERRADIuoBChNBcGlEYXRhc291cmNlQ29uZmlnEhAKCGJhc2VfdXJsGAEgASgJEhUKDXJlc291cmNlX3BhdGgYAiABKAkSPAoPY3JlZGVudGlhbF9raW5kGAMgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5BcGlDcmVkZW50aWFsS2luZBIZChFjcmVkZW50aWFsX2hlYWRlchgEIAEoCRIeChZjcmVkZW50aWFsX3F1ZXJ5X3BhcmFtGAUgASgJEjEKBm9hdXRoMhgGIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnIkEKF0NyYXdsZXJEYXRhc291cmNlQ29uZmlnEhMKC3NpdGVtYXBfdXJsGAEgASgJEhEKCW1heF9wYWdlcxgCIAEoDSKGAQoWVXBsb2FkRGF0YXNvdXJjZUNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIOCgZyZWdpb24YAiABKAkSDgoGYnVja2V0GAMgASgJEg4KBnByZWZpeBgEIAEoCRIVCg1hY2Nlc3Nfa2V5X2lkGAUgASgJEhMKC21heF9vYmplY3RzGAYgASgNIoMHCgpEYXRhc291cmNlEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRI2Cghwcm92aWRlchgDIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEjgKBmdpdGh1YhgFIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIyCgZzdGF0dXMYBiABKA4yIi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTdGF0dXMSGgoSd2ViaG9va19jb25maWd1cmVkGAcgASgIEi4KCmNyZWF0ZWRfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDmxhc3Rfc3luY2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCgNhcGkYCyABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWcSOgoHY3Jhd2xlchgMIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuQ3Jhd2xlckRhdGFzb3VyY2VDb25maWcSOAoGdXBsb2FkGA0gASgLMiguc2Fhcy5hY2NvdW50cy52MS5VcGxvYWREYXRhc291cmNlQ29uZmlnEhgKEGJvdW5kYXJ5X25vZGVfaWQYDiABKAkSNAoQbGFzdF9pbmdlc3RlZF9hdBgPIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoUbGFzdF9pbmdlc3RlZF9jb21taXQYECABKAkSFQoNc3RhdHVzX3JlYXNvbhgRIAEoCRIWCg5ib3VuZGFyeV9sYWJlbBgSIAEoCRISCgpjb25mb3JtYW50GBMgASgIEhcKD2NvbmZvcm1hbmNlX2dhcBgUIAEoCRI/Cg1saXZlX2RlbGl2ZXJ5GBUgASgOMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlTGl2ZURlbGl2ZXJ5EjUKEnJlY29uY2lsZV9pbnRlcnZhbBgWIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkoECAQQBVIRdGFyZ2V0X2NvbGxlY3Rpb24isgMKFkFkZEdpdEh1YlNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI7CgRyZXBvGAIgASgJQi26SCpyKBADGP8BMiFeW0EtWmEtejAtOV8uLV0rL1tBLVphLXowLTlfLi1dKyQSIAoFcGF0aHMYAyADKAlCEbpIDpIBCxBAIgdyBRABGIAEEhgKBmJyYW5jaBgEIAEoCUIIukgFcgMY/wESJAoQYm91bmRhcnlfbm9kZV9pZBgIIAEoCUIIukgFcgOwAQFIABImChBjb2xsZWN0aW9uX2xhYmVsGAkgASgJQgq6SAdyBRABGP8BSAASHgoMYWNjZXNzX3Rva2VuGAYgASgJQgi6SAVyAxiACBIgCg53ZWJob29rX3NlY3JldBgHIAEoCUIIukgFcgMYgAgSSQoPZmlsZV9leHRlbnNpb25zGAogAygJQjC6SC2SASoQICImciQQAhghMh5eXC5bQS1aYS16MC05XVtBLVphLXowLTkuXy1dKiRCEQoIYm91bmRhcnkSBbpIAggBSgQIBRAGUhF0YXJnZXRfY29sbGVjdGlvbiJLChdBZGRHaXRIdWJTb3VyY2VSZXNwb25zZRIwCgpkYXRhc291cmNlGAEgASgLMhwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlIsgEChBBZGRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESQgoIcHJvdmlkZXIYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckIKukgHggEEEAEgABI6CgZnaXRodWIYBCABKAsyKC5zYWFzLmFjY291bnRzLnYxLkdpdEh1YkRhdGFzb3VyY2VDb25maWdIABI0CgNhcGkYBSABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWdIABI8CgdjcmF3bGVyGAggASgLMikuc2Fhcy5hY2NvdW50cy52MS5DcmF3bGVyRGF0YXNvdXJjZUNvbmZpZ0gAEjoKBnVwbG9hZBgJIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuVXBsb2FkRGF0YXNvdXJjZUNvbmZpZ0gAEiQKEGJvdW5kYXJ5X25vZGVfaWQYDCABKAlCCLpIBXIDsAEBSAESJgoQY29sbGVjdGlvbl9sYWJlbBgNIAEoCUIKukgHcgUQARj/AUgBEhwKCmNyZWRlbnRpYWwYBiABKAlCCLpIBXIDGIAgEiAKDndlYmhvb2tfc2VjcmV0GAcgASgJQgi6SAVyAxiACBImChRvYXV0aDJfY2xpZW50X3NlY3JldBgKIAEoCUIIukgFcgMYgAhCCAoGY29uZmlnQhEKCGJvdW5kYXJ5EgW6SAIIAUoECAMQBFIRdGFyZ2V0X2NvbGxlY3Rpb24iRQoRQWRkU291cmNlUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJaChVEYXRhc291cmNlQ29uZmlnRmllbGQSCwoDa2V5GAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIMCgRoZWxwGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIsQBChlEYXRhc291cmNlQ29ubmVjdG9yQnVkZ2V0EhoKEm1heF9pdGVtc19wZXJfY2FsbBgBIAEoDRIaChJtYXhfYnl0ZXNfcGVyX2NhbGwYAiABKAMSFgoObWF4X2l0ZW1fYnl0ZXMYAyABKAMSHQoVb3BlcmF0aW9uc19wZXJfd2luZG93GAQgASgNEhYKDndpbmRvd19zZWNvbmRzGAUgASgDEiAKGGJhY2tncm91bmRfc2hhcmVfcGVyY2VudBgGIAEoDSKqBgocRGF0YXNvdXJjZVByb3ZpZGVyRGVzY3JpcHRvchI2Cghwcm92aWRlchgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRI+Cg1jb25maWdfZmllbGRzGAQgAygLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ29uZmlnRmllbGQSGAoQc3VwcG9ydHNfd2ViaG9vaxgFIAEoCBJHChpzdXBwb3J0ZWRfY3JlZGVudGlhbF9raW5kcxgGIAMoDjIjLnNhYXMuYWNjb3VudHMudjEuQXBpQ3JlZGVudGlhbEtpbmQSEQoJY29ubmVjdG9yGAcgASgJEkEKCWludGVyZmFjZRgIIAEoDjIuLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRJEChBjcmVkZW50aWFsX21vZGVzGAkgAygOMiouc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSPwoNcmVhZGVyc19tb2RlbBgKIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVJlYWRlcnNNb2RlbBI7CgZidWRnZXQYCyABKAsyKy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VDb25uZWN0b3JCdWRnZXQSEgoKY29uZm9ybWFudBgMIAEoCBIXCg9jb25mb3JtYW5jZV9nYXAYDSABKAkSGwoTYWNjZXB0c19uZXdfc291cmNlcxgOIAEoCBIgChhsaXZlX2RlbGl2ZXJ5X2NvbmZpZ3VyZWQYDyABKAgSGgoSYWNjZXB0c19vcGVyYXRpb25zGBAgASgIEhYKDm9wZXJhdGlvbnNfZ2FwGBEgASgJEhsKE29hdXRoX2F1dGhvcml6ZV91cmwYEiABKAkSFwoPb2F1dGhfdG9rZW5fdXJsGBMgASgJEhQKDG9hdXRoX3Njb3BlcxgUIAMoCSIdChtHZXREYXRhc291cmNlQ2F0YWxvZ1JlcXVlc3QiYQocR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZRJBCglwcm92aWRlcnMYASADKAsyLi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckRlc2NyaXB0b3IiLgoSTGlzdFNvdXJjZXNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiSAoTTGlzdFNvdXJjZXNSZXNwb25zZRIxCgtkYXRhc291cmNlcxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJCChBHZXRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIkUKEUdldFNvdXJjZVJlc3BvbnNlEjAKCmRhdGFzb3VyY2UYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2UidgoRU3luY1NvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQESHgoMYWNjZXNzX3Rva2VuGAMgASgJQgi6SAVyAxiAIBIRCglyZWNvbm5lY3QYBCABKAgiJAoSU3luY1NvdXJjZVJlc3BvbnNlEg4KBmpvYl9pZBgBIAEoCSJqChRHZXRTb3VyY2VTeW5jUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoGam9iX2lkGAMgASgJQgu6SAjYAQFyA7ABASKVAQoSU291cmNlU3luY0RlbGl2ZXJ5EhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESLQoFc3RhdGUYAiABKA4yFi5zYWFzLmpvYnMudjEuSm9iU3RhdGVCBrpIA8gBARI2CglleGVjdXRpb24YAyABKAsyIy5zYWFzLmpvYnMudjEuSm9iRXhlY3V0aW9uUmVmZXJlbmNlIosBChFTb3VyY2VTeW5jQ2hhbmdlcxINCgVmaWxlcxgBIAEoDRINCgVhZGRlZBgCIAEoDRIQCghtb2RpZmllZBgDIAEoDRIPCgdkZWxldGVkGAQgASgNEhMKC3NwbGl0X2tub3duGAUgASgIEhAKCHNuYXBzaG90GAYgASgIEg4KBmNvbW1pdBgHIAEoCSKtAQoRU291cmNlU3luY0ZhaWx1cmUSOQoGcmVhc29uGAEgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jRmFpbHVyZVJlYXNvbhIMCgRjb2RlGAIgASgJEg8KB21lc3NhZ2UYAyABKAkSEAoIcmV0cnlpbmcYBCABKAgSLAoIcmV0cnlfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoQEChJTb3VyY2VTeW5jUHJvZ3Jlc3MSMAoFcGhhc2UYASABKA4yIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNQaGFzZRI0Cgd0cmlnZ2VyGAIgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jVHJpZ2dlchItCglxdWV1ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2ZldGNoaW5nX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtjb21waWxlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMQoNaGFuZGVkX29mZl9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZmluaXNoZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjQKB2NoYW5nZXMYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNDaGFuZ2VzEjQKB2ZhaWx1cmUYCSABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNGYWlsdXJlEg8KB2F0dGVtcHQYCiABKA0SFAoMbWF4X2F0dGVtcHRzGAsgASgNItIBChVHZXRTb3VyY2VTeW5jUmVzcG9uc2USGAoGam9iX2lkGAEgASgJQgi6SAVyA7ABARItCgVzdGF0ZRgCIAEoDjIWLnNhYXMuam9icy52MS5Kb2JTdGF0ZUIGukgDyAEBEjgKCmRlbGl2ZXJpZXMYAyADKAsyJC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNEZWxpdmVyeRI2Cghwcm9ncmVzcxgEIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1Byb2dyZXNzIkUKE0RlbGV0ZVNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiFgoURGVsZXRlU291cmNlUmVzcG9uc2UiVgoTR2l0SHViQXBwUmVwb3NpdG9yeRIMCgRyZXBvGAEgASgJEhYKDmRlZmF1bHRfYnJhbmNoGAIgASgJEhkKEWFscmVhZHlfY29ubmVjdGVkGAMgASgIIjYKGkJlZ2luR2l0SHViQXBwU2V0dXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEicQobQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlEhMKC2luc3RhbGxfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIpwBCh1Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGP8BEiwKD2luc3RhbGxhdGlvbl9pZBgDIAEoCUITukgQcg4QARggMgheWzAtOV0rJBIYCgRjb2RlGAQgASgJQgq6SAdyBRABGP8BInYKHkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSOwoMcmVwb3NpdG9yaWVzGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5HaXRIdWJBcHBSZXBvc2l0b3J5IlEKH01pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiVAogTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSLEAQoVRGF0YXNvdXJjZUFjY291bnRMaW5rEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRIPCgd1c2VyX2lkGAMgASgJEhEKCWNvbm5lY3RvchgEIAEoCRIbChNwcm92aWRlcl9hY2NvdW50X2lkGAUgASgJEh4KFnByb3ZpZGVyX2FjY291bnRfbG9naW4YBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAitwEKFkRhdGFzb3VyY2VHcm91cEJpbmRpbmcSCgoCaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhEKCWNvbm5lY3RvchgDIAEoCRIZChFwcm92aWRlcl9ncm91cF9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJEhIKCmNyZWF0ZWRfYnkYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilAIKGERhdGFzb3VyY2VWZXJpZmllZERvbWFpbhIKCgJpZBgBIAEoCRIOCgZvcmdfaWQYAiABKAkSDgoGZG9tYWluGAMgASgJEjgKBnN0YXR1cxgEIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURvbWFpblN0YXR1cxIXCg90eHRfcmVjb3JkX25hbWUYBSABKAkSGAoQdHh0X3JlY29yZF92YWx1ZRgGIAEoCRIvCgt2ZXJpZmllZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKY3JlYXRlZF9hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiMwoXRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCSKmAQohQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSHgoMcmVkaXJlY3RfdXJpGAMgASgJQgi6SAVyAxiAEBIRCglzb3VyY2VfaWQYBCABKAkiegoiQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRIVCg1hdXRob3JpemVfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInUKJENvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGIBAEhgKBGNvZGUYAyABKAlCCrpIB3IFEAEY/wEiXgolQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRI1CgRsaW5rGAEgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQWNjb3VudExpbmsiPwojTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJeCiRMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2USNgoFbGlua3MYASADKAsyJy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VBY2NvdW50TGluayJUCiJEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIiUKI0RlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIjkKHUdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiiwIKHkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXNwb25zZRI2CgVsaW5rcxgBIAMoCzInLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUFjY291bnRMaW5rEjoKCGJpbmRpbmdzGAIgAygLMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlR3JvdXBCaW5kaW5nEjsKB2RvbWFpbnMYAyADKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbhI4CgV0ZWFtcxgEIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0irgEKGkJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSJQoRcHJvdmlkZXJfZ3JvdXBfaWQYAyABKAlCCrpIB3IFEAEY/wESGQoHdGVhbV9pZBgEIAEoCUIIukgFcgOwAQEiWAobQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlEjkKB2JpbmRpbmcYASABKAsyKC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VHcm91cEJpbmRpbmciTgocVW5iaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIfCh1VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJUChxDbGFpbURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGgoGZG9tYWluGAIgASgJQgq6SAdyBRADGP0BIlsKHUNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlEjoKBmRvbWFpbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVZlcmlmaWVkRG9tYWluIk8KHVZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlwKHlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXNwb25zZRI6CgZkb21haW4YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbiJPCh1EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIgCh5EZWxldGVEYXRhc291cmNlRG9tYWluUmVzcG9uc2UingIKEFNvdXJjZURlbGVnYXRpb24SCgoCaWQYASABKAkSEQoJc291cmNlX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIOCgZtb2R1bGUYBCABKAkSDwoHYmluZGluZxgFIAEoCRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpyZXZva2VkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBJACgpyZXZvY2F0aW9uGAggASgOMiwuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhISCgpyZXZva2VkX2J5GAkgASgJIoEBChxMaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIAoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABAUgAiAEBEhcKD2luY2x1ZGVfcmV2b2tlZBgDIAEoCEIMCgpfc291cmNlX2lkIlgKHUxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlEjcKC2RlbGVnYXRpb25zGAEgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIk8KHVJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlgKHlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZRI2CgpkZWxlZ2F0aW9uGAEgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIjIKD1RyaWdnZXJJZGVudGl0eRIOCgZzb3VyY2UYASABKAkSDwoHYmluZGluZxgCIAEoCSKEAQoXRGVjbGFyZWRUcmlnZ2VyU2NoZWR1bGUSDAoEY3JvbhgBIAEoCRIQCgh0aW1lem9uZRgCIAEoCRISCgptaXNzZWRfcnVuGAMgASgJEg8KB292ZXJsYXAYBCABKAkSEAoIZGVhZGxpbmUYBSABKAMSEgoKaW5wdXRfanNvbhgGIAEoCSLnAgoaVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USEQoJcmVmZXJlbmNlGAEgASgJEhUKDWRlbGVnYXRpb25faWQYAiABKAkSDgoGbW9kdWxlGAMgASgJEg8KB2JpbmRpbmcYBCABKAkSEAoIYXVkaWVuY2UYBSABKAkSDgoGdGVuYW50GAYgASgJEhoKEm93bmVyX3ByaW5jaXBhbF9pZBgHIAEoCRIOCgZzY29wZXMYCCADKAkSDgoGYWN0aXZlGAkgASgIEi4KCmNyZWF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnJldm9rZWRfYXQYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEkAKCnJldm9jYXRpb24YDCABKA4yLC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uIr4BChNUcmlnZ2VyQ2hpbGRDb250ZXh0EhUKDWRlbGVnYXRpb25faWQYASABKAkSDgoGbW9kdWxlGAIgASgJEg8KB2JpbmRpbmcYAyABKAkSEAoIYXVkaWVuY2UYBCABKAkSDgoGc2NvcGVzGAUgAygJEg4KBmxvb2t1cBgGIAEoCBItCgltaW50ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBnJlY29yZBgIIAEoCSLNAgoYU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEg8KB3JlbGVhc2UYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEjsKCHNjaGVkdWxlGAMgASgLMikuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlZFRyaWdnZXJTY2hlZHVsZRIzCghpZGVudGl0eRgEIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlcklkZW50aXR5Eg4KBmRpZ2VzdBgFIAEoCRI2ChJsYXN0X29jY3VycmVuY2VfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjYKEm5leHRfb2NjdXJyZW5jZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTdW5kZWNsYXJhYmxlX3JlYXNvbhgIIAEoCSJWCh1EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQEiYwoeRGVsZWdhdGVTb3VyY2VUcmlnZ2Vyc1Jlc3BvbnNlEkEKC2RlbGVnYXRpb25zGAEgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5UcmlnZ2VyRGVsZWdhdGlvblJlZmVyZW5jZSJbCiJHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABASLiAQojR2V0U291cmNlVHJpZ2dlckRlY2xhcmF0aW9uUmVzcG9uc2USPwoLZGVjbGFyYXRpb24YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvbhJBCgtkZWxlZ2F0aW9ucxgCIAMoCzIsLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USNwoIY2hpbGRyZW4YAyADKAsyJS5zYWFzLmFjY291bnRzLnYxLlRyaWdnZXJDaGlsZENvbnRleHQibgoWUmVjb25jaWxlU291cmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESHQoJZWZmZWN0X2lkGAMgASgJQgq6SAdyBRABGIABIkwKF1JlY29uY2lsZVNvdXJjZVJlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIOCgZzdGF0dXMYAiABKAkSDgoGam9iX2lkGAMgASgJIj0KHExvb2t1cFJlY29uY2lsZVNvdXJjZVJlcXVlc3QSHQoJZWZmZWN0X2lkGAEgASgJQgq6SAdyBRABGIABIvECCg9Tb3VyY2VPcGVyYXRpb24SDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIOCgZtZXRob2QYAyABKAkSDAoEcGF0aBgEIAEoCRINCgVxdWVyeRgFIAMoCRItCgxpbnB1dF9zY2hlbWEYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0Ei4KDW91dHB1dF9zY2hlbWEYByABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EjgKBmVmZmVjdBgIIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uLkVmZmVjdBIYChBtYXhfb3V0cHV0X2J5dGVzGAkgASgNEg4KBmRpZ2VzdBgKIAEoCSJLCgZFZmZlY3QSFgoSRUZGRUNUX1VOU1BFQ0lGSUVEEAASFAoQRUZGRUNUX1JFQURfT05MWRABEhMKD0VGRkVDVF9NVVRBVElPThACIo4BCh5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEjUKCm9wZXJhdGlvbnMYAyADKAsyIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvbiJYCh9EZWNsYXJlU291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlEjUKCm9wZXJhdGlvbnMYASADKAsyIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvbiJUChtMaXN0U291cmNlT3BlcmF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBIlUKHExpc3RTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2USNQoKb3BlcmF0aW9ucxgBIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIsMBChxJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABARIwCglvcGVyYXRpb24YAyABKAlCHbpIGnIYMhZeW2Etel1bYS16MC05X117MCw2M30kEh0KCmlucHV0X2pzb24YBCABKAlCCbpIBnIEKICABBIbCgllZmZlY3RfaWQYBSABKAlCCLpIBXIDKIABIn8KFlNvdXJjZU9wZXJhdGlvblJlY2VpcHQSEQoJZWZmZWN0X2lkGAEgASgJEhQKDGNvbW1pdHRlZF9hdBgCIAEoCRIOCgZzdGF0dXMYAyABKAkSFwoPcHJvdmlkZXJfc3RhdHVzGAQgASgNEhMKC291dHB1dF9qc29uGAUgASgJIm8KHUludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlEhMKC291dHB1dF9qc29uGAEgASgJEjkKB3JlY2VpcHQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvblJlY2VpcHQiQwoiTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEogAEieQojUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEhsKCWVmZmVjdF9pZBgDIAEoCUIIukgFcgMogAEiWwokUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIQCghyZWNlaXB0cxgCIAEoAxIOCgZzdGF0dXMYAyABKAkiSgopTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QSHQoJZWZmZWN0X2lkGAEgASgJQgq6SAdyBRABKIABKrcBChJEYXRhc291cmNlUHJvdmlkZXISIwofREFUQVNPVVJDRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEh4KGkRBVEFTT1VSQ0VfUFJPVklERVJfR0lUSFVCEAESGwoXREFUQVNPVVJDRV9QUk9WSURFUl9BUEkQAhIfChtEQVRBU09VUkNFX1BST1ZJREVSX0NSQVdMRVIQAxIeChpEQVRBU09VUkNFX1BST1ZJREVSX1VQTE9BRBAEKpEBChBEYXRhc291cmNlU3RhdHVzEiEKHURBVEFTT1VSQ0VfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHAoYREFUQVNPVVJDRV9TVEFUVVNfQUNUSVZFEAESHAoYREFUQVNPVVJDRV9TVEFUVVNfUEFVU0VEEAISHgoaREFUQVNPVVJDRV9TVEFUVVNfREVHUkFERUQQAyq8AQoWRGF0YXNvdXJjZUxpdmVEZWxpdmVyeRIoCiREQVRBU09VUkNFX0xJVkVfREVMSVZFUllfVU5TUEVDSUZJRUQQABIhCh1EQVRBU09VUkNFX0xJVkVfREVMSVZFUllfTk9ORRABEisKJ0RBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9TT1VSQ0VfV0VCSE9PSxACEigKJERBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9BUFBfV0VCSE9PSxADKtYBChFBcGlDcmVkZW50aWFsS2luZBIjCh9BUElfQ1JFREVOVElBTF9LSU5EX1VOU1BFQ0lGSUVEEAASHgoaQVBJX0NSRURFTlRJQUxfS0lORF9CRUFSRVIQARIdChlBUElfQ1JFREVOVElBTF9LSU5EX0JBU0lDEAISHgoaQVBJX0NSRURFTlRJQUxfS0lORF9IRUFERVIQAxIdChlBUElfQ1JFREVOVElBTF9LSU5EX1FVRVJZEAQSHgoaQVBJX0NSRURFTlRJQUxfS0lORF9PQVVUSDIQBSqmAgocRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRIuCipEQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfVU5TUEVDSUZJRUQQABIoCiREQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfRklMRVMQARIoCiREQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfUEFHRVMQAhIqCiZEQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfUkVDT1JEUxADEisKJ0RBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9NRVNTQUdFUxAEEikKJURBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9FVkVOVFMQBSrsAQoYRGF0YXNvdXJjZUNyZWRlbnRpYWxNb2RlEioKJkRBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX1VOU1BFQ0lGSUVEEAASIwofREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfTk9ORRABEiYKIkRBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX09SR19BUFAQAhIpCiVEQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9VU0VSX09BVVRIEAMSLAooREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfU1RBVElDX1NFQ1JFVBAEKpcBChZEYXRhc291cmNlUmVhZGVyc01vZGVsEigKJERBVEFTT1VSQ0VfUkVBREVSU19NT0RFTF9VTlNQRUNJRklFRBAAEioKJkRBVEFTT1VSQ0VfUkVBREVSU19NT0RFTF9TT1VSQ0VfU0NPUEVEEAESJwojREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1RSQU5TTEFURUQQAiruAQoPU291cmNlU3luY1BoYXNlEiEKHVNPVVJDRV9TWU5DX1BIQVNFX1VOU1BFQ0lGSUVEEAASHAoYU09VUkNFX1NZTkNfUEhBU0VfUVVFVUVEEAESHgoaU09VUkNFX1NZTkNfUEhBU0VfRkVUQ0hJTkcQAhIeChpTT1VSQ0VfU1lOQ19QSEFTRV9DT01QSUxFRBADEiAKHFNPVVJDRV9TWU5DX1BIQVNFX0hBTkRFRF9PRkYQBBIaChZTT1VSQ0VfU1lOQ19QSEFTRV9ET05FEAUSHAoYU09VUkNFX1NZTkNfUEhBU0VfRkFJTEVEEAYqpgMKF1NvdXJjZVN5bmNGYWlsdXJlUmVhc29uEioKJlNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX1VOU1BFQ0lGSUVEEAASKwonU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fUkFURV9MSU1JVEVEEAESKQolU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fQ1JFREVOVElBTBACEiwKKFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0FDQ0VTU19ERU5JRUQQAxIoCiRTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9OT1RfRk9VTkQQBBIoCiRTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9UT09fTEFSR0UQBRIvCitTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9IT1NUX1VOQVZBSUxBQkxFEAYSJAogU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fT1RIRVIQBxIuCipTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9ERUxJVkVSWV9GQUlMRUQQCCqcAQoRU291cmNlU3luY1RyaWdnZXISIwofU09VUkNFX1NZTkNfVFJJR0dFUl9VTlNQRUNJRklFRBAAEh4KGlNPVVJDRV9TWU5DX1RSSUdHRVJfTUFOVUFMEAESIQodU09VUkNFX1NZTkNfVFJJR0dFUl9TQ0hFRFVMRUQQAhIfChtTT1VSQ0VfU1lOQ19UUklHR0VSX1dFQkhPT0sQAyqPAQoWRGF0YXNvdXJjZURvbWFpblN0YXR1cxIoCiREQVRBU09VUkNFX0RPTUFJTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIkCiBEQVRBU09VUkNFX0RPTUFJTl9TVEFUVVNfUEVORElORxABEiUKIURBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19WRVJJRklFRBACKpUDChpTb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhIsCihTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1VOU1BFQ0lGSUVEEAASKAokU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9SRVZPS0VEEAESKQolU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9SRVBMQUNFRBACEi8KK1NPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fU09VUkNFX0RFTEVURUQQAxIvCitTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX01FTUJFUl9SRU1PVkVEEAQSMAosU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9QRVJNSVNTSU9OX0xPU1QQBRIuCipTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1VTRVJfSU5BQ1RJVkUQBhIwCixTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX0JJTkRJTkdfQ0hBTkdFRBAHMok4ChFEYXRhc291cmNlU2VydmljZRK5AwocUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0cxI1LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLlBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNSZXNwb25zZSKpAsLzGGwIAhADGhFkYXRhc291cmNlOmludm9rZSIRZGF0YXNvdXJjZTppbnZva2UqDwoJc291cmNlX2lkEAQYAzABOiMKH3NhYXMuZGF0YXNvdXJjZS5yZWNlaXB0cy5wcnVuZWQQAkADSARQA1gDYAHa8xi0AQoCCDwSAwjYBBgFIgIIAioLVU5BVkFJTEFCTEUqElJFU09VUkNFX0VYSEFVU1RFRCoHQUJPUlRFRDIPc2Fhcy1kYXRhc291cmNlSkYvc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlU2VydmljZS9Mb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUIAgWIAgYAFyGAoGc291cmNlEgZpbnZva2USBHJlYWQYARLmAQoiTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0cxI7LnNhYXMuYWNjb3VudHMudjEuTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLlBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNSZXNwb25zZSJLwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUANYA2ABEvwDChVJbnZva2VTb3VyY2VPcGVyYXRpb24SLi5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlIoEDwvMYbggCEAMaEWRhdGFzb3VyY2U6aW52b2tlIhFkYXRhc291cmNlOmludm9rZSoPCglzb3VyY2VfaWQQBBgDMAE6JQohc2Fhcy5kYXRhc291cmNlLm9wZXJhdGlvbi5pbnZva2VkEARAA0gEUARYBGAB2vMYrwEKAgg8EgMI2AQYBSICCAIqC1VOQVZBSUxBQkxFKhJSRVNPVVJDRV9FWEhBVVNURUQqB0FCT1JURUQyD3NhYXMtZGF0YXNvdXJjZUo/L3NhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVNlcnZpY2UvTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uUICABFiAgEBgAXIYCgZzb3VyY2USBmludm9rZRIEcmVhZBgBgtPkkwJVOgEqIlAvdjEvb3JnYW5pemF0aW9ucy97b3JnX2lkfS9kYXRhc291cmNlcy97c291cmNlX2lkfS9vcGVyYXRpb25zL3tvcGVyYXRpb259Omludm9rZRKHAgobTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uEjQuc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5JbnZva2VTb3VyY2VPcGVyYXRpb25SZXNwb25zZSKAAcLzGEcIAhADGg9kYXRhc291cmNlOnJlYWQiD2RhdGFzb3VyY2U6cmVhZCoPCgllZmZlY3RfaWQQBBgDMAE6AhABQAFIA1ADWARgAYLT5JMCLxItL3YxL2RhdGFzb3VyY2Utb3BlcmF0aW9uLXJlY2VpcHRzL3tlZmZlY3RfaWR9EoUCChRMaXN0U291cmNlT3BlcmF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdFNvdXJjZU9wZXJhdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlIo0BwvMYRAgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwI/Ej0vdjEvb3JnYW5pemF0aW9ucy97b3JnX2lkfS9kYXRhc291cmNlcy97c291cmNlX2lkfS9vcGVyYXRpb25zEpMCChdEZWNsYXJlU291cmNlT3BlcmF0aW9ucxIwLnNhYXMuYWNjb3VudHMudjEuRGVjbGFyZVNvdXJjZU9wZXJhdGlvbnNSZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlIpIBwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5vcGVyYXRpb24uZGVjbGFyZWQQAkABSARQA1gDYAGC0+STAkI6ASoiPS92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMSrAEKD0FkZEdpdEh1YlNvdXJjZRIoLnNhYXMuYWNjb3VudHMudjEuQWRkR2l0SHViU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuQWRkR2l0SHViU291cmNlUmVzcG9uc2UiRMLzGEAIAhAEKgwKBm9yZ19pZBACGAEwATogChxzYWFzLmRhdGFzb3VyY2Uuc291cmNlLmFkZGVkEAJAAUgEUARYA2ABEpoBCglBZGRTb3VyY2USIi5zYWFzLmFjY291bnRzLnYxLkFkZFNvdXJjZVJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLkFkZFNvdXJjZVJlc3BvbnNlIkTC8xhACAIQBCoMCgZvcmdfaWQQAhgBMAE6IAocc2Fhcy5kYXRhc291cmNlLnNvdXJjZS5hZGRlZBACQAFIBFAEWANgARKPAQoUR2V0RGF0YXNvdXJjZUNhdGFsb2cSLS5zYWFzLmFjY291bnRzLnYxLkdldERhdGFzb3VyY2VDYXRhbG9nUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgDUAJYAmABEoIBCgtMaXN0U291cmNlcxIkLnNhYXMuYWNjb3VudHMudjEuTGlzdFNvdXJjZXNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlc1Jlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARJ8CglHZXRTb3VyY2USIi5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVJlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKeAQoKU3luY1NvdXJjZRIjLnNhYXMuYWNjb3VudHMudjEuU3luY1NvdXJjZVJlcXVlc3QaJC5zYWFzLmFjY291bnRzLnYxLlN5bmNTb3VyY2VSZXNwb25zZSJFwvMYQQgCEAQqDAoGb3JnX2lkEAIYATABOiEKHXNhYXMuZGF0YXNvdXJjZS5zb3VyY2Uuc3luY2VkEAJAAUgEUARYAmABEogBCg1HZXRTb3VyY2VTeW5jEiYuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VTeW5jUmVxdWVzdBonLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlU3luY1Jlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKlAQoMRGVsZXRlU291cmNlEiUuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVTb3VyY2VSZXF1ZXN0GiYuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVTb3VyY2VSZXNwb25zZSJGwvMYQggCEAQqDAoGb3JnX2lkEAIYATABOiIKHnNhYXMuZGF0YXNvdXJjZS5zb3VyY2UucmVtb3ZlZBACQAFIBFACWAJgARLEAQoTQmVnaW5HaXRIdWJBcHBTZXR1cBIsLnNhYXMuYWNjb3VudHMudjEuQmVnaW5HaXRIdWJBcHBTZXR1cFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkJlZ2luR2l0SHViQXBwU2V0dXBSZXNwb25zZSJQwvMYTAgCEAQqDAoGb3JnX2lkEAIYATABOiwKKHNhYXMuZGF0YXNvdXJjZS5naXRodWJfYXBwLnNldHVwX3N0YXJ0ZWQQAkABSARQAlgEYAESzwEKFkNvbXBsZXRlR2l0SHViQXBwU2V0dXASLy5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVzcG9uc2UiUsLzGE4IAhAEKgwKBm9yZ19pZBACGAEwATouCipzYWFzLmRhdGFzb3VyY2UuZ2l0aHViX2FwcC5zZXR1cF9jb21wbGV0ZWQQAkABSARQBFgDYAESzQEKGE1pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcBIxLnNhYXMuYWNjb3VudHMudjEuTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVxdWVzdBoyLnNhYXMuYWNjb3VudHMudjEuTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2UiSsLzGEYIAhAEKgwKBm9yZ19pZBACGAEwATomCiJzYWFzLmRhdGFzb3VyY2UuY3JlZGVudGlhbC51cGRhdGVkEAJAAUgEUAJYA2ABEtUBChpCZWdpbkRhdGFzb3VyY2VBY2NvdW50TGluaxIzLnNhYXMuYWNjb3VudHMudjEuQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0GjQuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkRhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkzC8xhICAIQAyoMCgZvcmdfaWQQAhgBMAE6KAokc2Fhcy5kYXRhc291cmNlLmFjY291bnRfbGlua19zdGFydGVkEAJAAUgEUAJYBGABEtgBCh1Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGluaxI2LnNhYXMuYWNjb3VudHMudjEuQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkbC8xhCCAIQAyoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmFjY291bnRfbGlua2VkEAJAAUgEUARYA2ABErUBChxMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzEjUuc2Fhcy5hY2NvdW50cy52MS5MaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1Jlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARLUAQobRGVsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rEjQuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZSJIwvMYRAgCEAMqDAoGb3JnX2lkEAIYATABOiQKIHNhYXMuZGF0YXNvdXJjZS5hY2NvdW50X3VubGlua2VkEAJAAUgEUAJYA2ABEqMBChZHZXREYXRhc291cmNlRGlyZWN0b3J5Ei8uc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlRGlyZWN0b3J5UmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZURpcmVjdG9yeVJlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARK3AQoTQmluZERhdGFzb3VyY2VHcm91cBIsLnNhYXMuYWNjb3VudHMudjEuQmluZERhdGFzb3VyY2VHcm91cFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJDwvMYPwgCEAQqDAoGb3JnX2lkEAIYATABOh8KG3NhYXMuZGF0YXNvdXJjZS5ncm91cF9ib3VuZBACQAFIBFACWANgARK/AQoVVW5iaW5kRGF0YXNvdXJjZUdyb3VwEi4uc2Fhcy5hY2NvdW50cy52MS5VbmJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJFwvMYQQgCEAQqDAoGb3JnX2lkEAIYATABOiEKHXNhYXMuZGF0YXNvdXJjZS5ncm91cF91bmJvdW5kEAJAAUgEUAJYA2ABEsABChVDbGFpbURhdGFzb3VyY2VEb21haW4SLi5zYWFzLmFjY291bnRzLnYxLkNsYWltRGF0YXNvdXJjZURvbWFpblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmRvbWFpbl9jbGFpbWVkEAJAAUgEUAJYA2ABEsQBChZWZXJpZnlEYXRhc291cmNlRG9tYWluEi8uc2Fhcy5hY2NvdW50cy52MS5WZXJpZnlEYXRhc291cmNlRG9tYWluUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuVmVyaWZ5RGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkfC8xhDCAIQBCoMCgZvcmdfaWQQAhgBMAE6Iwofc2Fhcy5kYXRhc291cmNlLmRvbWFpbl92ZXJpZmllZBACQAFIBFACWANgARLDAQoWRGVsZXRlRGF0YXNvdXJjZURvbWFpbhIvLnNhYXMuYWNjb3VudHMudjEuRGVsZXRlRGF0YXNvdXJjZURvbWFpblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VEb21haW5SZXNwb25zZSJGwvMYQggCEAQqDAoGb3JnX2lkEAIYATABOiIKHnNhYXMuZGF0YXNvdXJjZS5kb21haW5fcmVtb3ZlZBACQAFIBFACWANgARKgAQoVTGlzdFNvdXJjZURlbGVnYXRpb25zEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlRGVsZWdhdGlvbnNSZXNwb25zZSImwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQAlgDYAESxwEKFlJldm9rZVNvdXJjZURlbGVnYXRpb24SLy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTb3VyY2VEZWxlZ2F0aW9uUmVzcG9uc2UiSsLzGEYIAhAEKgwKBm9yZ19pZBACGAEwATomCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5yZXZva2VkEAJAAUgEUAJYA2ABEusBChZEZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzEi8uc2Fhcy5hY2NvdW50cy52MS5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuRGVsZWdhdGVTb3VyY2VUcmlnZ2Vyc1Jlc3BvbnNlIm7C8xhqCAIQBCoMCgZvcmdfaWQQAhgBMAE6Sgoic2Fhcy5kYXRhc291cmNlLmRlbGVnYXRpb24uY3JlYXRlZAoic2Fhcy5kYXRhc291cmNlLmRlbGVnYXRpb24ucmV2b2tlZBACQAFIBFACWANgARKyAQobR2V0U291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEjQuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXNwb25zZSImwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQAlgDYAESgwMKD1JlY29uY2lsZVNvdXJjZRIoLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVzcG9uc2UimgLC8xhqCAIQAxoRZGF0YXNvdXJjZTppbnZva2UiEWRhdGFzb3VyY2U6aW52b2tlKg8KCXNvdXJjZV9pZBAEGAMwATohCh1zYWFzLmRhdGFzb3VyY2Uuc291cmNlLnN5bmNlZBACQANIBFACWAJgAdrzGKcBCgIIPBIDCNgEGAUiAggCKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMg9zYWFzLWRhdGFzb3VyY2VKOS9zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTZXJ2aWNlL0xvb2t1cFJlY29uY2lsZVNvdXJjZVCAIFiAIGABchgKBnNvdXJjZRIGaW52b2tlEgRyZWFkGAESvwEKFUxvb2t1cFJlY29uY2lsZVNvdXJjZRIuLnNhYXMuYWNjb3VudHMudjEuTG9va3VwUmVjb25jaWxlU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVzcG9uc2UiS8LzGEcIAhADGg9kYXRhc291cmNlOnJlYWQiD2RhdGFzb3VyY2U6cmVhZCoPCgllZmZlY3RfaWQQBBgDMAE6AhABQAFIA1ACWAJgAWIGcHJvdG8z", [file_buf_validate_validate, file_codefly_runnable_v0_options, file_google_api_annotations, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiFzYWFzL2FjY291bnRzL3YxL2RhdGFzb3VyY2UucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoWR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIMCgRyZXBvGAEgASgJEg0KBXBhdGhzGAIgAygJEg4KBmJyYW5jaBgDIAEoCRIXCg9maWxlX2V4dGVuc2lvbnMYBCADKAki9AEKD0FwaU9BdXRoMkNvbmZpZxI2CgVncmFudBgEIAEoDjInLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnLkdyYW50EhEKCXRva2VuX3VybBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSDgoGc2NvcGVzGAMgAygJInMKBUdyYW50EhUKEUdSQU5UX1VOU1BFQ0lGSUVEEAASFwoTR1JBTlRfUkVGUkVTSF9UT0tFThABEhwKGEdSQU5UX0NMSUVOVF9DUkVERU5USUFMUxACEhwKGEdSQU5UX0FVVEhPUklaQVRJT05fQ09ERRADIuoBChNBcGlEYXRhc291cmNlQ29uZmlnEhAKCGJhc2VfdXJsGAEgASgJEhUKDXJlc291cmNlX3BhdGgYAiABKAkSPAoPY3JlZGVudGlhbF9raW5kGAMgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5BcGlDcmVkZW50aWFsS2luZBIZChFjcmVkZW50aWFsX2hlYWRlchgEIAEoCRIeChZjcmVkZW50aWFsX3F1ZXJ5X3BhcmFtGAUgASgJEjEKBm9hdXRoMhgGIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnIkEKF0NyYXdsZXJEYXRhc291cmNlQ29uZmlnEhMKC3NpdGVtYXBfdXJsGAEgASgJEhEKCW1heF9wYWdlcxgCIAEoDSKGAQoWVXBsb2FkRGF0YXNvdXJjZUNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIOCgZyZWdpb24YAiABKAkSDgoGYnVja2V0GAMgASgJEg4KBnByZWZpeBgEIAEoCRIVCg1hY2Nlc3Nfa2V5X2lkGAUgASgJEhMKC21heF9vYmplY3RzGAYgASgNIoMHCgpEYXRhc291cmNlEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRI2Cghwcm92aWRlchgDIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEjgKBmdpdGh1YhgFIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIyCgZzdGF0dXMYBiABKA4yIi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTdGF0dXMSGgoSd2ViaG9va19jb25maWd1cmVkGAcgASgIEi4KCmNyZWF0ZWRfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDmxhc3Rfc3luY2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCgNhcGkYCyABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWcSOgoHY3Jhd2xlchgMIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuQ3Jhd2xlckRhdGFzb3VyY2VDb25maWcSOAoGdXBsb2FkGA0gASgLMiguc2Fhcy5hY2NvdW50cy52MS5VcGxvYWREYXRhc291cmNlQ29uZmlnEhgKEGJvdW5kYXJ5X25vZGVfaWQYDiABKAkSNAoQbGFzdF9pbmdlc3RlZF9hdBgPIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoUbGFzdF9pbmdlc3RlZF9jb21taXQYECABKAkSFQoNc3RhdHVzX3JlYXNvbhgRIAEoCRIWCg5ib3VuZGFyeV9sYWJlbBgSIAEoCRISCgpjb25mb3JtYW50GBMgASgIEhcKD2NvbmZvcm1hbmNlX2dhcBgUIAEoCRI/Cg1saXZlX2RlbGl2ZXJ5GBUgASgOMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlTGl2ZURlbGl2ZXJ5EjUKEnJlY29uY2lsZV9pbnRlcnZhbBgWIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkoECAQQBVIRdGFyZ2V0X2NvbGxlY3Rpb24isgMKFkFkZEdpdEh1YlNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI7CgRyZXBvGAIgASgJQi26SCpyKBADGP8BMiFeW0EtWmEtejAtOV8uLV0rL1tBLVphLXowLTlfLi1dKyQSIAoFcGF0aHMYAyADKAlCEbpIDpIBCxBAIgdyBRABGIAEEhgKBmJyYW5jaBgEIAEoCUIIukgFcgMY/wESJAoQYm91bmRhcnlfbm9kZV9pZBgIIAEoCUIIukgFcgOwAQFIABImChBjb2xsZWN0aW9uX2xhYmVsGAkgASgJQgq6SAdyBRABGP8BSAASHgoMYWNjZXNzX3Rva2VuGAYgASgJQgi6SAVyAxiACBIgCg53ZWJob29rX3NlY3JldBgHIAEoCUIIukgFcgMYgAgSSQoPZmlsZV9leHRlbnNpb25zGAogAygJQjC6SC2SASoQICImciQQAhghMh5eXC5bQS1aYS16MC05XVtBLVphLXowLTkuXy1dKiRCEQoIYm91bmRhcnkSBbpIAggBSgQIBRAGUhF0YXJnZXRfY29sbGVjdGlvbiJLChdBZGRHaXRIdWJTb3VyY2VSZXNwb25zZRIwCgpkYXRhc291cmNlGAEgASgLMhwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlIsgEChBBZGRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESQgoIcHJvdmlkZXIYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckIKukgHggEEEAEgABI6CgZnaXRodWIYBCABKAsyKC5zYWFzLmFjY291bnRzLnYxLkdpdEh1YkRhdGFzb3VyY2VDb25maWdIABI0CgNhcGkYBSABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWdIABI8CgdjcmF3bGVyGAggASgLMikuc2Fhcy5hY2NvdW50cy52MS5DcmF3bGVyRGF0YXNvdXJjZUNvbmZpZ0gAEjoKBnVwbG9hZBgJIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuVXBsb2FkRGF0YXNvdXJjZUNvbmZpZ0gAEiQKEGJvdW5kYXJ5X25vZGVfaWQYDCABKAlCCLpIBXIDsAEBSAESJgoQY29sbGVjdGlvbl9sYWJlbBgNIAEoCUIKukgHcgUQARj/AUgBEhwKCmNyZWRlbnRpYWwYBiABKAlCCLpIBXIDGIAgEiAKDndlYmhvb2tfc2VjcmV0GAcgASgJQgi6SAVyAxiACBImChRvYXV0aDJfY2xpZW50X3NlY3JldBgKIAEoCUIIukgFcgMYgAhCCAoGY29uZmlnQhEKCGJvdW5kYXJ5EgW6SAIIAUoECAMQBFIRdGFyZ2V0X2NvbGxlY3Rpb24iRQoRQWRkU291cmNlUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJaChVEYXRhc291cmNlQ29uZmlnRmllbGQSCwoDa2V5GAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIMCgRoZWxwGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIsQBChlEYXRhc291cmNlQ29ubmVjdG9yQnVkZ2V0EhoKEm1heF9pdGVtc19wZXJfY2FsbBgBIAEoDRIaChJtYXhfYnl0ZXNfcGVyX2NhbGwYAiABKAMSFgoObWF4X2l0ZW1fYnl0ZXMYAyABKAMSHQoVb3BlcmF0aW9uc19wZXJfd2luZG93GAQgASgNEhYKDndpbmRvd19zZWNvbmRzGAUgASgDEiAKGGJhY2tncm91bmRfc2hhcmVfcGVyY2VudBgGIAEoDSKqBgocRGF0YXNvdXJjZVByb3ZpZGVyRGVzY3JpcHRvchI2Cghwcm92aWRlchgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRI+Cg1jb25maWdfZmllbGRzGAQgAygLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ29uZmlnRmllbGQSGAoQc3VwcG9ydHNfd2ViaG9vaxgFIAEoCBJHChpzdXBwb3J0ZWRfY3JlZGVudGlhbF9raW5kcxgGIAMoDjIjLnNhYXMuYWNjb3VudHMudjEuQXBpQ3JlZGVudGlhbEtpbmQSEQoJY29ubmVjdG9yGAcgASgJEkEKCWludGVyZmFjZRgIIAEoDjIuLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRJEChBjcmVkZW50aWFsX21vZGVzGAkgAygOMiouc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSPwoNcmVhZGVyc19tb2RlbBgKIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVJlYWRlcnNNb2RlbBI7CgZidWRnZXQYCyABKAsyKy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VDb25uZWN0b3JCdWRnZXQSEgoKY29uZm9ybWFudBgMIAEoCBIXCg9jb25mb3JtYW5jZV9nYXAYDSABKAkSGwoTYWNjZXB0c19uZXdfc291cmNlcxgOIAEoCBIgChhsaXZlX2RlbGl2ZXJ5X2NvbmZpZ3VyZWQYDyABKAgSGgoSYWNjZXB0c19vcGVyYXRpb25zGBAgASgIEhYKDm9wZXJhdGlvbnNfZ2FwGBEgASgJEhsKE29hdXRoX2F1dGhvcml6ZV91cmwYEiABKAkSFwoPb2F1dGhfdG9rZW5fdXJsGBMgASgJEhQKDG9hdXRoX3Njb3BlcxgUIAMoCSIdChtHZXREYXRhc291cmNlQ2F0YWxvZ1JlcXVlc3QiYQocR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZRJBCglwcm92aWRlcnMYASADKAsyLi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckRlc2NyaXB0b3IiLgoSTGlzdFNvdXJjZXNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiSAoTTGlzdFNvdXJjZXNSZXNwb25zZRIxCgtkYXRhc291cmNlcxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJCChBHZXRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIkUKEUdldFNvdXJjZVJlc3BvbnNlEjAKCmRhdGFzb3VyY2UYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2UidgoRU3luY1NvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQESHgoMYWNjZXNzX3Rva2VuGAMgASgJQgi6SAVyAxiAIBIRCglyZWNvbm5lY3QYBCABKAgiJAoSU3luY1NvdXJjZVJlc3BvbnNlEg4KBmpvYl9pZBgBIAEoCSJqChRHZXRTb3VyY2VTeW5jUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoGam9iX2lkGAMgASgJQgu6SAjYAQFyA7ABASKVAQoSU291cmNlU3luY0RlbGl2ZXJ5EhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESLQoFc3RhdGUYAiABKA4yFi5zYWFzLmpvYnMudjEuSm9iU3RhdGVCBrpIA8gBARI2CglleGVjdXRpb24YAyABKAsyIy5zYWFzLmpvYnMudjEuSm9iRXhlY3V0aW9uUmVmZXJlbmNlIosBChFTb3VyY2VTeW5jQ2hhbmdlcxINCgVmaWxlcxgBIAEoDRINCgVhZGRlZBgCIAEoDRIQCghtb2RpZmllZBgDIAEoDRIPCgdkZWxldGVkGAQgASgNEhMKC3NwbGl0X2tub3duGAUgASgIEhAKCHNuYXBzaG90GAYgASgIEg4KBmNvbW1pdBgHIAEoCSKtAQoRU291cmNlU3luY0ZhaWx1cmUSOQoGcmVhc29uGAEgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jRmFpbHVyZVJlYXNvbhIMCgRjb2RlGAIgASgJEg8KB21lc3NhZ2UYAyABKAkSEAoIcmV0cnlpbmcYBCABKAgSLAoIcmV0cnlfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoQEChJTb3VyY2VTeW5jUHJvZ3Jlc3MSMAoFcGhhc2UYASABKA4yIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNQaGFzZRI0Cgd0cmlnZ2VyGAIgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jVHJpZ2dlchItCglxdWV1ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2ZldGNoaW5nX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtjb21waWxlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMQoNaGFuZGVkX29mZl9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZmluaXNoZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjQKB2NoYW5nZXMYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNDaGFuZ2VzEjQKB2ZhaWx1cmUYCSABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNGYWlsdXJlEg8KB2F0dGVtcHQYCiABKA0SFAoMbWF4X2F0dGVtcHRzGAsgASgNItIBChVHZXRTb3VyY2VTeW5jUmVzcG9uc2USGAoGam9iX2lkGAEgASgJQgi6SAVyA7ABARItCgVzdGF0ZRgCIAEoDjIWLnNhYXMuam9icy52MS5Kb2JTdGF0ZUIGukgDyAEBEjgKCmRlbGl2ZXJpZXMYAyADKAsyJC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNEZWxpdmVyeRI2Cghwcm9ncmVzcxgEIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1Byb2dyZXNzIkUKE0RlbGV0ZVNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiFgoURGVsZXRlU291cmNlUmVzcG9uc2UiVgoTR2l0SHViQXBwUmVwb3NpdG9yeRIMCgRyZXBvGAEgASgJEhYKDmRlZmF1bHRfYnJhbmNoGAIgASgJEhkKEWFscmVhZHlfY29ubmVjdGVkGAMgASgIIjYKGkJlZ2luR2l0SHViQXBwU2V0dXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEicQobQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlEhMKC2luc3RhbGxfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIpwBCh1Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGP8BEiwKD2luc3RhbGxhdGlvbl9pZBgDIAEoCUITukgQcg4QARggMgheWzAtOV0rJBIYCgRjb2RlGAQgASgJQgq6SAdyBRABGP8BInYKHkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSOwoMcmVwb3NpdG9yaWVzGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5HaXRIdWJBcHBSZXBvc2l0b3J5IlEKH01pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiVAogTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSLEAQoVRGF0YXNvdXJjZUFjY291bnRMaW5rEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRIPCgd1c2VyX2lkGAMgASgJEhEKCWNvbm5lY3RvchgEIAEoCRIbChNwcm92aWRlcl9hY2NvdW50X2lkGAUgASgJEh4KFnByb3ZpZGVyX2FjY291bnRfbG9naW4YBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAitwEKFkRhdGFzb3VyY2VHcm91cEJpbmRpbmcSCgoCaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhEKCWNvbm5lY3RvchgDIAEoCRIZChFwcm92aWRlcl9ncm91cF9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJEhIKCmNyZWF0ZWRfYnkYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilAIKGERhdGFzb3VyY2VWZXJpZmllZERvbWFpbhIKCgJpZBgBIAEoCRIOCgZvcmdfaWQYAiABKAkSDgoGZG9tYWluGAMgASgJEjgKBnN0YXR1cxgEIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURvbWFpblN0YXR1cxIXCg90eHRfcmVjb3JkX25hbWUYBSABKAkSGAoQdHh0X3JlY29yZF92YWx1ZRgGIAEoCRIvCgt2ZXJpZmllZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKY3JlYXRlZF9hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiMwoXRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCSKmAQohQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSHgoMcmVkaXJlY3RfdXJpGAMgASgJQgi6SAVyAxiAEBIRCglzb3VyY2VfaWQYBCABKAkiegoiQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRIVCg1hdXRob3JpemVfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInUKJENvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGIBAEhgKBGNvZGUYAyABKAlCCrpIB3IFEAEY/wEiXgolQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRI1CgRsaW5rGAEgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQWNjb3VudExpbmsiPwojTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJeCiRMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2USNgoFbGlua3MYASADKAsyJy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VBY2NvdW50TGluayJUCiJEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIiUKI0RlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIjkKHUdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiiwIKHkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXNwb25zZRI2CgVsaW5rcxgBIAMoCzInLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUFjY291bnRMaW5rEjoKCGJpbmRpbmdzGAIgAygLMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlR3JvdXBCaW5kaW5nEjsKB2RvbWFpbnMYAyADKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbhI4CgV0ZWFtcxgEIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0irgEKGkJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSJQoRcHJvdmlkZXJfZ3JvdXBfaWQYAyABKAlCCrpIB3IFEAEY/wESGQoHdGVhbV9pZBgEIAEoCUIIukgFcgOwAQEiWAobQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlEjkKB2JpbmRpbmcYASABKAsyKC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VHcm91cEJpbmRpbmciTgocVW5iaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIfCh1VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJUChxDbGFpbURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGgoGZG9tYWluGAIgASgJQgq6SAdyBRADGP0BIlsKHUNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlEjoKBmRvbWFpbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVZlcmlmaWVkRG9tYWluIk8KHVZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlwKHlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXNwb25zZRI6CgZkb21haW4YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbiJPCh1EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIgCh5EZWxldGVEYXRhc291cmNlRG9tYWluUmVzcG9uc2UingIKEFNvdXJjZURlbGVnYXRpb24SCgoCaWQYASABKAkSEQoJc291cmNlX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIOCgZtb2R1bGUYBCABKAkSDwoHYmluZGluZxgFIAEoCRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpyZXZva2VkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBJACgpyZXZvY2F0aW9uGAggASgOMiwuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhISCgpyZXZva2VkX2J5GAkgASgJIoEBChxMaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIAoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABAUgAiAEBEhcKD2luY2x1ZGVfcmV2b2tlZBgDIAEoCEIMCgpfc291cmNlX2lkIlgKHUxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlEjcKC2RlbGVnYXRpb25zGAEgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIk8KHVJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlgKHlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZRI2CgpkZWxlZ2F0aW9uGAEgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIjIKD1RyaWdnZXJJZGVudGl0eRIOCgZzb3VyY2UYASABKAkSDwoHYmluZGluZxgCIAEoCSKEAQoXRGVjbGFyZWRUcmlnZ2VyU2NoZWR1bGUSDAoEY3JvbhgBIAEoCRIQCgh0aW1lem9uZRgCIAEoCRISCgptaXNzZWRfcnVuGAMgASgJEg8KB292ZXJsYXAYBCABKAkSEAoIZGVhZGxpbmUYBSABKAMSEgoKaW5wdXRfanNvbhgGIAEoCSLnAgoaVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USEQoJcmVmZXJlbmNlGAEgASgJEhUKDWRlbGVnYXRpb25faWQYAiABKAkSDgoGbW9kdWxlGAMgASgJEg8KB2JpbmRpbmcYBCABKAkSEAoIYXVkaWVuY2UYBSABKAkSDgoGdGVuYW50GAYgASgJEhoKEm93bmVyX3ByaW5jaXBhbF9pZBgHIAEoCRIOCgZzY29wZXMYCCADKAkSDgoGYWN0aXZlGAkgASgIEi4KCmNyZWF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnJldm9rZWRfYXQYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEkAKCnJldm9jYXRpb24YDCABKA4yLC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uIr4BChNUcmlnZ2VyQ2hpbGRDb250ZXh0EhUKDWRlbGVnYXRpb25faWQYASABKAkSDgoGbW9kdWxlGAIgASgJEg8KB2JpbmRpbmcYAyABKAkSEAoIYXVkaWVuY2UYBCABKAkSDgoGc2NvcGVzGAUgAygJEg4KBmxvb2t1cBgGIAEoCBItCgltaW50ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBnJlY29yZBgIIAEoCSKgAwoYU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEg8KB3JlbGVhc2UYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEjsKCHNjaGVkdWxlGAMgASgLMikuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlZFRyaWdnZXJTY2hlZHVsZRIzCghpZGVudGl0eRgEIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlcklkZW50aXR5Eg4KBmRpZ2VzdBgFIAEoCRI2ChJsYXN0X29jY3VycmVuY2VfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjYKEm5leHRfb2NjdXJyZW5jZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTdW5kZWNsYXJhYmxlX3JlYXNvbhgIIAEoCRIaChJob3N0X3N3ZWVwX2VuYWJsZWQYCSABKAgSNQoRaG9zdF9zd2VlcF9kdWVfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIlYKHURlbGVnYXRlU291cmNlVHJpZ2dlcnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABASJjCh5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVzcG9uc2USQQoLZGVsZWdhdGlvbnMYASADKAsyLC5zYWFzLmFjY291bnRzLnYxLlRyaWdnZXJEZWxlZ2F0aW9uUmVmZXJlbmNlIlsKIkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBIuIBCiNHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXNwb25zZRI/CgtkZWNsYXJhdGlvbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEkEKC2RlbGVnYXRpb25zGAIgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5UcmlnZ2VyRGVsZWdhdGlvblJlZmVyZW5jZRI3CghjaGlsZHJlbhgDIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlckNoaWxkQ29udGV4dCJuChZSZWNvbmNpbGVTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABARIdCgllZmZlY3RfaWQYAyABKAlCCrpIB3IFEAEYgAEiTAoXUmVjb25jaWxlU291cmNlUmVzcG9uc2USEQoJZWZmZWN0X2lkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIOCgZqb2JfaWQYAyABKAkiPQocTG9va3VwUmVjb25jaWxlU291cmNlUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEYgAEi8QIKD1NvdXJjZU9wZXJhdGlvbhIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg4KBm1ldGhvZBgDIAEoCRIMCgRwYXRoGAQgASgJEg0KBXF1ZXJ5GAUgAygJEi0KDGlucHV0X3NjaGVtYRgGIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSLgoNb3V0cHV0X3NjaGVtYRgHIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSOAoGZWZmZWN0GAggASgOMiguc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VPcGVyYXRpb24uRWZmZWN0EhgKEG1heF9vdXRwdXRfYnl0ZXMYCSABKA0SDgoGZGlnZXN0GAogASgJIksKBkVmZmVjdBIWChJFRkZFQ1RfVU5TUEVDSUZJRUQQABIUChBFRkZFQ1RfUkVBRF9PTkxZEAESEwoPRUZGRUNUX01VVEFUSU9OEAIijgEKHkRlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESNQoKb3BlcmF0aW9ucxgDIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIlgKH0RlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2USNQoKb3BlcmF0aW9ucxgBIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIlQKG0xpc3RTb3VyY2VPcGVyYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQEiVQocTGlzdFNvdXJjZU9wZXJhdGlvbnNSZXNwb25zZRI1CgpvcGVyYXRpb25zGAEgAygLMiEuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VPcGVyYXRpb24iwwEKHEludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEjAKCW9wZXJhdGlvbhgDIAEoCUIdukgachgyFl5bYS16XVthLXowLTlfXXswLDYzfSQSHQoKaW5wdXRfanNvbhgEIAEoCUIJukgGcgQogIAEEhsKCWVmZmVjdF9pZBgFIAEoCUIIukgFcgMogAEifwoWU291cmNlT3BlcmF0aW9uUmVjZWlwdBIRCgllZmZlY3RfaWQYASABKAkSFAoMY29tbWl0dGVkX2F0GAIgASgJEg4KBnN0YXR1cxgDIAEoCRIXCg9wcm92aWRlcl9zdGF0dXMYBCABKA0SEwoLb3V0cHV0X2pzb24YBSABKAkibwodSW52b2tlU291cmNlT3BlcmF0aW9uUmVzcG9uc2USEwoLb3V0cHV0X2pzb24YASABKAkSOQoHcmVjZWlwdBgCIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uUmVjZWlwdCJDCiJMb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0Eh0KCWVmZmVjdF9pZBgBIAEoCUIKukgHcgUQASiAASJ5CiNQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoJZWZmZWN0X2lkGAMgASgJQgi6SAVyAyiAASJbCiRQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVzcG9uc2USEQoJZWZmZWN0X2lkGAEgASgJEhAKCHJlY2VpcHRzGAIgASgDEg4KBnN0YXR1cxgDIAEoCSJKCilMb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEogAEqtwEKEkRhdGFzb3VyY2VQcm92aWRlchIjCh9EQVRBU09VUkNFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASHgoaREFUQVNPVVJDRV9QUk9WSURFUl9HSVRIVUIQARIbChdEQVRBU09VUkNFX1BST1ZJREVSX0FQSRACEh8KG0RBVEFTT1VSQ0VfUFJPVklERVJfQ1JBV0xFUhADEh4KGkRBVEFTT1VSQ0VfUFJPVklERVJfVVBMT0FEEAQqkQEKEERhdGFzb3VyY2VTdGF0dXMSIQodREFUQVNPVVJDRV9TVEFUVVNfVU5TUEVDSUZJRUQQABIcChhEQVRBU09VUkNFX1NUQVRVU19BQ1RJVkUQARIcChhEQVRBU09VUkNFX1NUQVRVU19QQVVTRUQQAhIeChpEQVRBU09VUkNFX1NUQVRVU19ERUdSQURFRBADKrwBChZEYXRhc291cmNlTGl2ZURlbGl2ZXJ5EigKJERBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEiEKHURBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9OT05FEAESKwonREFUQVNPVVJDRV9MSVZFX0RFTElWRVJZX1NPVVJDRV9XRUJIT09LEAISKAokREFUQVNPVVJDRV9MSVZFX0RFTElWRVJZX0FQUF9XRUJIT09LEAMq1gEKEUFwaUNyZWRlbnRpYWxLaW5kEiMKH0FQSV9DUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIeChpBUElfQ1JFREVOVElBTF9LSU5EX0JFQVJFUhABEh0KGUFQSV9DUkVERU5USUFMX0tJTkRfQkFTSUMQAhIeChpBUElfQ1JFREVOVElBTF9LSU5EX0hFQURFUhADEh0KGUFQSV9DUkVERU5USUFMX0tJTkRfUVVFUlkQBBIeChpBUElfQ1JFREVOVElBTF9LSU5EX09BVVRIMhAFKqYCChxEYXRhc291cmNlQ29ubmVjdG9ySW50ZXJmYWNlEi4KKkRBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9VTlNQRUNJRklFRBAAEigKJERBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9GSUxFUxABEigKJERBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9QQUdFUxACEioKJkRBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9SRUNPUkRTEAMSKwonREFUQVNPVVJDRV9DT05ORUNUT1JfSU5URVJGQUNFX01FU1NBR0VTEAQSKQolREFUQVNPVVJDRV9DT05ORUNUT1JfSU5URVJGQUNFX0VWRU5UUxAFKuwBChhEYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSKgomREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfVU5TUEVDSUZJRUQQABIjCh9EQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9OT05FEAESJgoiREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfT1JHX0FQUBACEikKJURBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX1VTRVJfT0FVVEgQAxIsCihEQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9TVEFUSUNfU0VDUkVUEAQqlwEKFkRhdGFzb3VyY2VSZWFkZXJzTW9kZWwSKAokREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1VOU1BFQ0lGSUVEEAASKgomREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1NPVVJDRV9TQ09QRUQQARInCiNEQVRBU09VUkNFX1JFQURFUlNfTU9ERUxfVFJBTlNMQVRFRBACKu4BCg9Tb3VyY2VTeW5jUGhhc2USIQodU09VUkNFX1NZTkNfUEhBU0VfVU5TUEVDSUZJRUQQABIcChhTT1VSQ0VfU1lOQ19QSEFTRV9RVUVVRUQQARIeChpTT1VSQ0VfU1lOQ19QSEFTRV9GRVRDSElORxACEh4KGlNPVVJDRV9TWU5DX1BIQVNFX0NPTVBJTEVEEAMSIAocU09VUkNFX1NZTkNfUEhBU0VfSEFOREVEX09GRhAEEhoKFlNPVVJDRV9TWU5DX1BIQVNFX0RPTkUQBRIcChhTT1VSQ0VfU1lOQ19QSEFTRV9GQUlMRUQQBiqmAwoXU291cmNlU3luY0ZhaWx1cmVSZWFzb24SKgomU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fVU5TUEVDSUZJRUQQABIrCidTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9SQVRFX0xJTUlURUQQARIpCiVTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9DUkVERU5USUFMEAISLAooU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fQUNDRVNTX0RFTklFRBADEigKJFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX05PVF9GT1VORBAEEigKJFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX1RPT19MQVJHRRAFEi8KK1NPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0hPU1RfVU5BVkFJTEFCTEUQBhIkCiBTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9PVEhFUhAHEi4KKlNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0RFTElWRVJZX0ZBSUxFRBAIKpwBChFTb3VyY2VTeW5jVHJpZ2dlchIjCh9TT1VSQ0VfU1lOQ19UUklHR0VSX1VOU1BFQ0lGSUVEEAASHgoaU09VUkNFX1NZTkNfVFJJR0dFUl9NQU5VQUwQARIhCh1TT1VSQ0VfU1lOQ19UUklHR0VSX1NDSEVEVUxFRBACEh8KG1NPVVJDRV9TWU5DX1RSSUdHRVJfV0VCSE9PSxADKo8BChZEYXRhc291cmNlRG9tYWluU3RhdHVzEigKJERBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19VTlNQRUNJRklFRBAAEiQKIERBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19QRU5ESU5HEAESJQohREFUQVNPVVJDRV9ET01BSU5fU1RBVFVTX1ZFUklGSUVEEAIqlQMKGlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uEiwKKFNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fVU5TUEVDSUZJRUQQABIoCiRTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1JFVk9LRUQQARIpCiVTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1JFUExBQ0VEEAISLworU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9TT1VSQ0VfREVMRVRFRBADEi8KK1NPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fTUVNQkVSX1JFTU9WRUQQBBIwCixTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1BFUk1JU1NJT05fTE9TVBAFEi4KKlNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fVVNFUl9JTkFDVElWRRAGEjAKLFNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fQklORElOR19DSEFOR0VEEAcyiTgKEURhdGFzb3VyY2VTZXJ2aWNlErkDChxQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzEjUuc2Fhcy5hY2NvdW50cy52MS5QcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlIqkCwvMYbAgCEAMaEWRhdGFzb3VyY2U6aW52b2tlIhFkYXRhc291cmNlOmludm9rZSoPCglzb3VyY2VfaWQQBBgDMAE6Iwofc2Fhcy5kYXRhc291cmNlLnJlY2VpcHRzLnBydW5lZBACQANIBFADWANgAdrzGLQBCgIIPBIDCNgEGAUiAggCKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMg9zYWFzLWRhdGFzb3VyY2VKRi9zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTZXJ2aWNlL0xvb2t1cFBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNQgCBYgCBgAXIYCgZzb3VyY2USBmludm9rZRIEcmVhZBgBEuYBCiJMb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzEjsuc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlIkvC8xhHCAIQAxoPZGF0YXNvdXJjZTpyZWFkIg9kYXRhc291cmNlOnJlYWQqDwoJZWZmZWN0X2lkEAQYAzABOgIQAUABSANQA1gDYAES/AMKFUludm9rZVNvdXJjZU9wZXJhdGlvbhIuLnNhYXMuYWNjb3VudHMudjEuSW52b2tlU291cmNlT3BlcmF0aW9uUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuSW52b2tlU291cmNlT3BlcmF0aW9uUmVzcG9uc2UigQPC8xhuCAIQAxoRZGF0YXNvdXJjZTppbnZva2UiEWRhdGFzb3VyY2U6aW52b2tlKg8KCXNvdXJjZV9pZBAEGAMwATolCiFzYWFzLmRhdGFzb3VyY2Uub3BlcmF0aW9uLmludm9rZWQQBEADSARQBFgEYAHa8xivAQoCCDwSAwjYBBgFIgIIAioLVU5BVkFJTEFCTEUqElJFU09VUkNFX0VYSEFVU1RFRCoHQUJPUlRFRDIPc2Fhcy1kYXRhc291cmNlSj8vc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlU2VydmljZS9Mb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25QgIAEWICAQGABchgKBnNvdXJjZRIGaW52b2tlEgRyZWFkGAGC0+STAlU6ASoiUC92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMve29wZXJhdGlvbn06aW52b2tlEocCChtMb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb24SNC5zYWFzLmFjY291bnRzLnYxLkxvb2t1cEludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlIoABwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUANYBGABgtPkkwIvEi0vdjEvZGF0YXNvdXJjZS1vcGVyYXRpb24tcmVjZWlwdHMve2VmZmVjdF9pZH0ShQIKFExpc3RTb3VyY2VPcGVyYXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlT3BlcmF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2UijQHC8xhECAIQAxoPZGF0YXNvdXJjZTpyZWFkIg9kYXRhc291cmNlOnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAj8SPS92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMSkwIKF0RlY2xhcmVTb3VyY2VPcGVyYXRpb25zEjAuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1JlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkRlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2UikgHC8xhGCAIQBCoMCgZvcmdfaWQQAhgBMAE6Jgoic2Fhcy5kYXRhc291cmNlLm9wZXJhdGlvbi5kZWNsYXJlZBACQAFIBFADWANgAYLT5JMCQjoBKiI9L3YxL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZGF0YXNvdXJjZXMve3NvdXJjZV9pZH0vb3BlcmF0aW9ucxKsAQoPQWRkR2l0SHViU291cmNlEiguc2Fhcy5hY2NvdW50cy52MS5BZGRHaXRIdWJTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5BZGRHaXRIdWJTb3VyY2VSZXNwb25zZSJEwvMYQAgCEAQqDAoGb3JnX2lkEAIYATABOiAKHHNhYXMuZGF0YXNvdXJjZS5zb3VyY2UuYWRkZWQQAkABSARQBFgDYAESmgEKCUFkZFNvdXJjZRIiLnNhYXMuYWNjb3VudHMudjEuQWRkU291cmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuQWRkU291cmNlUmVzcG9uc2UiRMLzGEAIAhAEKgwKBm9yZ19pZBACGAEwATogChxzYWFzLmRhdGFzb3VyY2Uuc291cmNlLmFkZGVkEAJAAUgEUARYA2ABEo8BChRHZXREYXRhc291cmNlQ2F0YWxvZxItLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlQ2F0YWxvZ1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSANQAlgCYAESggEKC0xpc3RTb3VyY2VzEiQuc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VzUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEnwKCUdldFNvdXJjZRIiLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEp4BCgpTeW5jU291cmNlEiMuc2Fhcy5hY2NvdW50cy52MS5TeW5jU291cmNlUmVxdWVzdBokLnNhYXMuYWNjb3VudHMudjEuU3luY1NvdXJjZVJlc3BvbnNlIkXC8xhBCAIQBCoMCgZvcmdfaWQQAhgBMAE6IQodc2Fhcy5kYXRhc291cmNlLnNvdXJjZS5zeW5jZWQQAkABSARQBFgCYAESiAEKDUdldFNvdXJjZVN5bmMSJi5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVN5bmNSZXF1ZXN0Gicuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VTeW5jUmVzcG9uc2UiJsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEqUBCgxEZWxldGVTb3VyY2USJS5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZVNvdXJjZVJlcXVlc3QaJi5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZVNvdXJjZVJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLnNvdXJjZS5yZW1vdmVkEAJAAUgEUAJYAmABEsQBChNCZWdpbkdpdEh1YkFwcFNldHVwEiwuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkdpdEh1YkFwcFNldHVwUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlIlDC8xhMCAIQBCoMCgZvcmdfaWQQAhgBMAE6LAooc2Fhcy5kYXRhc291cmNlLmdpdGh1Yl9hcHAuc2V0dXBfc3RhcnRlZBACQAFIBFACWARgARLPAQoWQ29tcGxldGVHaXRIdWJBcHBTZXR1cBIvLnNhYXMuYWNjb3VudHMudjEuQ29tcGxldGVHaXRIdWJBcHBTZXR1cFJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZSJSwvMYTggCEAQqDAoGb3JnX2lkEAIYATABOi4KKnNhYXMuZGF0YXNvdXJjZS5naXRodWJfYXBwLnNldHVwX2NvbXBsZXRlZBACQAFIBFAEWANgARLNAQoYTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwEjEuc2Fhcy5hY2NvdW50cy52MS5NaWdyYXRlR2l0SHViU291cmNlVG9BcHBSZXF1ZXN0GjIuc2Fhcy5hY2NvdW50cy52MS5NaWdyYXRlR2l0SHViU291cmNlVG9BcHBSZXNwb25zZSJKwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5jcmVkZW50aWFsLnVwZGF0ZWQQAkABSARQAlgDYAES1QEKGkJlZ2luRGF0YXNvdXJjZUFjY291bnRMaW5rEjMuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkRhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNC5zYWFzLmFjY291bnRzLnYxLkJlZ2luRGF0YXNvdXJjZUFjY291bnRMaW5rUmVzcG9uc2UiTMLzGEgIAhADKgwKBm9yZ19pZBACGAEwATooCiRzYWFzLmRhdGFzb3VyY2UuYWNjb3VudF9saW5rX3N0YXJ0ZWQQAkABSARQAlgEYAES2AEKHUNvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rEjYuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVzcG9uc2UiRsLzGEIIAhADKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLmRhdGFzb3VyY2UuYWNjb3VudF9saW5rZWQQAkABSARQBFgDYAEStQEKHExpc3RNeURhdGFzb3VyY2VBY2NvdW50TGlua3MSNS5zYWFzLmFjY291bnRzLnYxLkxpc3RNeURhdGFzb3VyY2VBY2NvdW50TGlua3NSZXF1ZXN0GjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEtQBChtEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmsSNC5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkjC8xhECAIQAyoMCgZvcmdfaWQQAhgBMAE6JAogc2Fhcy5kYXRhc291cmNlLmFjY291bnRfdW5saW5rZWQQAkABSARQAlgDYAESowEKFkdldERhdGFzb3VyY2VEaXJlY3RvcnkSLy5zYWFzLmFjY291bnRzLnYxLkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlRGlyZWN0b3J5UmVzcG9uc2UiJsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABErcBChNCaW5kRGF0YXNvdXJjZUdyb3VwEiwuc2Fhcy5hY2NvdW50cy52MS5CaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlIkPC8xg/CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hwobc2Fhcy5kYXRhc291cmNlLmdyb3VwX2JvdW5kEAJAAUgEUAJYA2ABEr8BChVVbmJpbmREYXRhc291cmNlR3JvdXASLi5zYWFzLmFjY291bnRzLnYxLlVuYmluZERhdGFzb3VyY2VHcm91cFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLlVuYmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlIkXC8xhBCAIQBCoMCgZvcmdfaWQQAhgBMAE6IQodc2Fhcy5kYXRhc291cmNlLmdyb3VwX3VuYm91bmQQAkABSARQAlgDYAESwAEKFUNsYWltRGF0YXNvdXJjZURvbWFpbhIuLnNhYXMuYWNjb3VudHMudjEuQ2xhaW1EYXRhc291cmNlRG9tYWluUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuQ2xhaW1EYXRhc291cmNlRG9tYWluUmVzcG9uc2UiRsLzGEIIAhAEKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLmRhdGFzb3VyY2UuZG9tYWluX2NsYWltZWQQAkABSARQAlgDYAESxAEKFlZlcmlmeURhdGFzb3VyY2VEb21haW4SLy5zYWFzLmFjY291bnRzLnYxLlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5WZXJpZnlEYXRhc291cmNlRG9tYWluUmVzcG9uc2UiR8LzGEMIAhAEKgwKBm9yZ19pZBACGAEwATojCh9zYWFzLmRhdGFzb3VyY2UuZG9tYWluX3ZlcmlmaWVkEAJAAUgEUAJYA2ABEsMBChZEZWxldGVEYXRhc291cmNlRG9tYWluEi8uc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuRGVsZXRlRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmRvbWFpbl9yZW1vdmVkEAJAAUgEUAJYA2ABEqABChVMaXN0U291cmNlRGVsZWdhdGlvbnMSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VEZWxlZ2F0aW9uc1JlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARLHAQoWUmV2b2tlU291cmNlRGVsZWdhdGlvbhIvLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU291cmNlRGVsZWdhdGlvblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZSJKwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5kZWxlZ2F0aW9uLnJldm9rZWQQAkABSARQAlgDYAES6wEKFkRlbGVnYXRlU291cmNlVHJpZ2dlcnMSLy5zYWFzLmFjY291bnRzLnYxLkRlbGVnYXRlU291cmNlVHJpZ2dlcnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVzcG9uc2UibsLzGGoIAhAEKgwKBm9yZ19pZBACGAEwATpKCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5jcmVhdGVkCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5yZXZva2VkEAJAAUgEUAJYA2ABErIBChtHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb24SNC5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKDAwoPUmVjb25jaWxlU291cmNlEiguc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXNwb25zZSKaAsLzGGoIAhADGhFkYXRhc291cmNlOmludm9rZSIRZGF0YXNvdXJjZTppbnZva2UqDwoJc291cmNlX2lkEAQYAzABOiEKHXNhYXMuZGF0YXNvdXJjZS5zb3VyY2Uuc3luY2VkEAJAA0gEUAJYAmAB2vMYpwEKAgg8EgMI2AQYBSICCAIqC1VOQVZBSUxBQkxFKhJSRVNPVVJDRV9FWEhBVVNURUQqB0FCT1JURUQyD3NhYXMtZGF0YXNvdXJjZUo5L3NhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVNlcnZpY2UvTG9va3VwUmVjb25jaWxlU291cmNlUIAgWIAgYAFyGAoGc291cmNlEgZpbnZva2USBHJlYWQYARK/AQoVTG9va3VwUmVjb25jaWxlU291cmNlEi4uc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBSZWNvbmNpbGVTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXNwb25zZSJLwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUAJYAmABYgZwcm90bzM", [file_buf_validate_validate, file_codefly_runnable_v0_options, file_google_api_annotations, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * GitHubDatasourceConfig is the public, non-secret configuration of a GitHub @@ -2416,6 +2416,16 @@ export const RevokeSourceDelegationResponseSchema: GenMessage vi.clearAllMocks()); @@ -69,7 +75,7 @@ async function admissions(): Promise { expect(found).not.toBeNull(); return found as HTMLElement; }); - await within(table).findByText(/Scheduled · next occurrence/); + await within(table).findByText(/Declared · next occurrence/); return table; } @@ -199,3 +205,70 @@ describe("SourceTriggerSchedules", () => { ).toHaveLength(0); }); }); + +// THE DECLARED OCCURRENCE AND THE PASS THAT WILL HAPPEN ARE TWO FACTS +// (issue #1121, N6). +// +// The panel used to show the cron's instant alone and claim the stopgap agreed +// with it. It does not: the cron comes due on the half hour while the sweep +// fires on the stored instant, so a reader was shown 09:30 for a pass that +// happens at 09:07. Both rows are here, and the host's row says what it is. +describe("the host's own sweep", () => { + it("shows the instant it will sweep on, beside the declared occurrence", async () => { + serve({ declaration: declared, delegations: [], children: [] }); + renderInApp(); + const table = await admissions(); + + const declaredRow = within(table).getByText(/Declared · next occurrence/); + const sweepRow = within(table).getByText( + /This host’s stopgap sweep · next pass/, + ); + expect(declaredRow).toBeTruthy(); + expect(sweepRow).toBeTruthy(); + // The two instants are different, which is the whole finding. Compared + // as rendered rather than as ISO strings, because the cells are local + // time. + const declaredAt = new Date("2026-10-12T09:30:00Z").toLocaleString(); + const sweepAt = new Date("2026-10-12T09:07:00Z").toLocaleString(); + expect(declaredAt).not.toBe(sweepAt); + expect( + table.querySelector('[data-slot="next-occurrence"]')?.textContent, + ).toBe(declaredAt); + expect(table.querySelector('[data-slot="host-sweep-due"]')?.textContent).toBe( + sweepAt, + ); + // The sweep admits no operation, so it has no effect identity to show — + // said rather than left as a dash that would read as "it has none". + expect( + within(table).getByText(/None — the sweep admits no operation/), + ).toBeTruthy(); + }); + + it("says the host takes no pass when an operator turned the sweep off", async () => { + // The switch exists so the declaration's consumer and the stopgap can + // never both run. With it off there is no host pass at all, and showing + // an instant would be describing one. + serve({ + declaration: { ...declared, hostSweepEnabled: false, hostSweepDueAt: undefined }, + delegations: [], + children: [], + }); + renderInApp(); + const table = await admissions(); + + expect( + within(table).getByText(/This host’s stopgap sweep · off/), + ).toBeTruthy(); + expect( + within(table).getByText(/An executing module fires the declaration/), + ).toBeTruthy(); + expect(table.querySelector('[data-slot="host-sweep-due"]')?.textContent).toBe( + "—", + ); + // The declaration itself is unchanged by the switch: it is what the + // source declares, not what this host does about it. + expect( + table.querySelector('[data-slot="next-occurrence"]')?.textContent, + ).toBe(new Date("2026-10-12T09:30:00Z").toLocaleString()); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx index 50cbe35ba..0e1d8314b 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx @@ -50,11 +50,21 @@ import { * "next occurrence" that would never happen, which is the most misleading * thing a schedule panel can show. * - * "Next" is computed from the declared cron and is what the declaration SAYS. - * Once a composed module fires the trigger the authoritative cursor is that - * module's and this host will not hold one; while the stopgap runs, the cron - * and the stored instant it sweeps on are derived from the same interval, so - * the two agree without either reading the other. + * "Next occurrence" is computed from the declared cron and is what the + * declaration SAYS. Once a composed module fires the trigger the authoritative + * cursor is that module's and this host will not hold one. + * + * WHILE THE STOPGAP RUNS, THAT IS NOT WHEN THE PASS HAPPENS (issue #1121, N6). + * The two share a period and not a phase: the declared cron for a sub-hourly + * interval steps by the minute inside the hour, due on the quarter or half + * hour, while the sweep + * fires on the source's stored instant, which every pass moves to + * `now + interval` — an arbitrary offset inside the period. This panel used to + * claim the two agreed "without either reading the other", which was wrong in + * the most misleading direction available: a reader was shown 14:30 for a pass + * that would happen at 14:07. So both are shown, each labelled with who acts + * on it, and the host's own row disappears when an operator turns the stopgap + * off. * * A person's "Sync now" is listed in the SAME table as those occurrences, and * that is the panel's claim rather than a convenience: both admit @@ -89,9 +99,11 @@ export function SourceTriggerSchedules({ orgId }: { orgId: string }) { Each connected source declares its reconcile schedule as a trigger an executing module is meant to fire. Until a composed runtime consumes that declaration, this host still fires the schedule itself, as a - stopgap. A person's “Sync now” admits the same - operation with an effect identity of its own, so it is listed beside - the occurrences rather than apart from them. + stopgap — and it fires on its own stored instant, not on the declared + cron, so the two rows below are two different facts. A person's + “Sync now” admits the same operation with an effect + identity of its own, so it is listed beside the occurrences rather + than apart from them. @@ -210,7 +222,7 @@ function SourceTriggerRow({ - Scheduled · next occurrence + Declared · next occurrence {MINTED_BY_THE_FIRING_MODULE} @@ -220,6 +232,31 @@ function SourceTriggerRow({ — + {/* + * What will actually happen, while this host still fires the + * schedule. It is a different instant from the row above, on + * purpose: see the panel's note. With the stopgap switched off + * the host takes no pass, so the row says that rather than + * showing a time. + */} + + + {declaration?.hostSweepEnabled + ? "This host’s stopgap sweep · next pass" + : "This host’s stopgap sweep · off"} + + + {declaration?.hostSweepEnabled + ? NO_EFFECT_IDENTITY + : "An executing module fires the declaration"} + + + {declaration?.hostSweepEnabled + ? instant(declaration?.hostSweepDueAt) + : "—"} + + — + )} {passes.map((pass) => ( @@ -266,14 +303,17 @@ const RECONCILE_OPERATION = * minted by the module that fires the trigger, so this host never sees one and * has nothing to print. A dash would read as "it has none", which would make * the admission look unlike the manual pass beside it. - * - * The stopgap sweep is a third case and is deliberately claimed as neither: it - * enqueues under a key derived from the schedule instant and admits no - * operation at all, so it has no effect identity to show and appears here only - * as a "last occurrence" moving. */ const MINTED_BY_THE_FIRING_MODULE = "Minted by the firing module"; +/** + * The stopgap sweep is a third case and claims neither: it enqueues under a key + * derived from the schedule instant and admits no operation at all, so it has + * no effect identity, and its pass shows up in "last occurrence" rather than as + * an admission of its own. + */ +const NO_EFFECT_IDENTITY = "None — the sweep admits no operation"; + /** * One labelled fact. Exported because the playground renders the same * declaration facts in its own card, and two copies of this would be two @@ -326,20 +366,25 @@ function useAdmittedPasses(): AdmittedReconcilePass[] { * state an installing module refuses rather than running the work as itself, * so it is said plainly instead of being left blank. * - * The reference arm does NOT name which module's delegation it is (issue - * #1112, m5). The reference value is the source id, so every delegation on one - * source reads back the same and the host resolves whichever module holds an - * active one at mint. Naming one here would be this page's own guess: an - * operator whose executing module holds no delegation would read that a - * reference is bound to it when another module's is. The references table - * names each module, which is where that question is answerable. + * The reference arm does NOT name which module's delegation it is. The + * reference value is the source id, so every delegation on one source reads + * back the same value and the host resolves it afresh on every mint; the + * references table names each module, which is where that question is + * answerable. + * + * What it now says instead is that the delegation named is one the executing + * module can use (issue #1121, m5): the host only sets the arm for a delegation + * whose binding names its own module as the audience, which is the one shape a + * source-id reference can serve. Before that, an operator whose executing + * module held no delegation read "bound" because some other module's existed — + * and every occurrence was refused at the mint. */ export function declaredIdentity(declaration?: { identity?: { source: string; binding: string }; }): string { const identity = declaration?.identity; if (identity?.source) - return "A delegation reference — whichever module holds an active one"; + return "A delegation reference the executing module holds"; if (identity?.binding) return "An installation binding"; return "None — occurrences would be refused"; } diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx index 82418429b..e8c89945c 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx @@ -2,7 +2,7 @@ import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react"; import { HttpResponse, http, type JsonBodyType } from "msw"; import { toast } from "sonner"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import Page from "@/app/admin/platform/unattended-work/page"; +import Page from "@/app/admin/unattended-work/page"; import type { OrgRole, PlatformRole } from "@/lib/auth-session"; import { renderInApp, rpc } from "@/test/container"; import { server } from "@/test/setup"; @@ -147,12 +147,13 @@ describe("UnattendedWorkPlayground", () => { // Which arm it names, said in words: a declaration that named neither // would have its occurrences refused, and the page says so rather than // leaving the field blank. - // It does not claim WHICH module's reference: the reference value is the - // source id, so the host resolves whichever module holds an active one. + // It does not claim WHICH module's reference — the reference value is the + // source id and the references table answers that — but it does say the + // delegation is one the executing module can use, because the host sets + // the arm only for a delegation whose binding names its own module as + // the audience (issue #1121, m5). expect( - screen.getByText( - "A delegation reference — whichever module holds an active one", - ), + screen.getByText("A delegation reference the executing module holds"), ).toBeTruthy(); // The reference an operator bound, with whose authority it carries and diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/datasource_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/datasource_pb.ts index 3675f0313..a667d68cb 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/datasource_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/datasource_pb.ts @@ -18,7 +18,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/datasource.proto. */ export const file_saas_accounts_v1_datasource: GenFile = /*@__PURE__*/ - fileDesc("CiFzYWFzL2FjY291bnRzL3YxL2RhdGFzb3VyY2UucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoWR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIMCgRyZXBvGAEgASgJEg0KBXBhdGhzGAIgAygJEg4KBmJyYW5jaBgDIAEoCRIXCg9maWxlX2V4dGVuc2lvbnMYBCADKAki9AEKD0FwaU9BdXRoMkNvbmZpZxI2CgVncmFudBgEIAEoDjInLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnLkdyYW50EhEKCXRva2VuX3VybBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSDgoGc2NvcGVzGAMgAygJInMKBUdyYW50EhUKEUdSQU5UX1VOU1BFQ0lGSUVEEAASFwoTR1JBTlRfUkVGUkVTSF9UT0tFThABEhwKGEdSQU5UX0NMSUVOVF9DUkVERU5USUFMUxACEhwKGEdSQU5UX0FVVEhPUklaQVRJT05fQ09ERRADIuoBChNBcGlEYXRhc291cmNlQ29uZmlnEhAKCGJhc2VfdXJsGAEgASgJEhUKDXJlc291cmNlX3BhdGgYAiABKAkSPAoPY3JlZGVudGlhbF9raW5kGAMgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5BcGlDcmVkZW50aWFsS2luZBIZChFjcmVkZW50aWFsX2hlYWRlchgEIAEoCRIeChZjcmVkZW50aWFsX3F1ZXJ5X3BhcmFtGAUgASgJEjEKBm9hdXRoMhgGIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnIkEKF0NyYXdsZXJEYXRhc291cmNlQ29uZmlnEhMKC3NpdGVtYXBfdXJsGAEgASgJEhEKCW1heF9wYWdlcxgCIAEoDSKGAQoWVXBsb2FkRGF0YXNvdXJjZUNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIOCgZyZWdpb24YAiABKAkSDgoGYnVja2V0GAMgASgJEg4KBnByZWZpeBgEIAEoCRIVCg1hY2Nlc3Nfa2V5X2lkGAUgASgJEhMKC21heF9vYmplY3RzGAYgASgNIoMHCgpEYXRhc291cmNlEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRI2Cghwcm92aWRlchgDIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEjgKBmdpdGh1YhgFIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIyCgZzdGF0dXMYBiABKA4yIi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTdGF0dXMSGgoSd2ViaG9va19jb25maWd1cmVkGAcgASgIEi4KCmNyZWF0ZWRfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDmxhc3Rfc3luY2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCgNhcGkYCyABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWcSOgoHY3Jhd2xlchgMIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuQ3Jhd2xlckRhdGFzb3VyY2VDb25maWcSOAoGdXBsb2FkGA0gASgLMiguc2Fhcy5hY2NvdW50cy52MS5VcGxvYWREYXRhc291cmNlQ29uZmlnEhgKEGJvdW5kYXJ5X25vZGVfaWQYDiABKAkSNAoQbGFzdF9pbmdlc3RlZF9hdBgPIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoUbGFzdF9pbmdlc3RlZF9jb21taXQYECABKAkSFQoNc3RhdHVzX3JlYXNvbhgRIAEoCRIWCg5ib3VuZGFyeV9sYWJlbBgSIAEoCRISCgpjb25mb3JtYW50GBMgASgIEhcKD2NvbmZvcm1hbmNlX2dhcBgUIAEoCRI/Cg1saXZlX2RlbGl2ZXJ5GBUgASgOMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlTGl2ZURlbGl2ZXJ5EjUKEnJlY29uY2lsZV9pbnRlcnZhbBgWIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkoECAQQBVIRdGFyZ2V0X2NvbGxlY3Rpb24isgMKFkFkZEdpdEh1YlNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI7CgRyZXBvGAIgASgJQi26SCpyKBADGP8BMiFeW0EtWmEtejAtOV8uLV0rL1tBLVphLXowLTlfLi1dKyQSIAoFcGF0aHMYAyADKAlCEbpIDpIBCxBAIgdyBRABGIAEEhgKBmJyYW5jaBgEIAEoCUIIukgFcgMY/wESJAoQYm91bmRhcnlfbm9kZV9pZBgIIAEoCUIIukgFcgOwAQFIABImChBjb2xsZWN0aW9uX2xhYmVsGAkgASgJQgq6SAdyBRABGP8BSAASHgoMYWNjZXNzX3Rva2VuGAYgASgJQgi6SAVyAxiACBIgCg53ZWJob29rX3NlY3JldBgHIAEoCUIIukgFcgMYgAgSSQoPZmlsZV9leHRlbnNpb25zGAogAygJQjC6SC2SASoQICImciQQAhghMh5eXC5bQS1aYS16MC05XVtBLVphLXowLTkuXy1dKiRCEQoIYm91bmRhcnkSBbpIAggBSgQIBRAGUhF0YXJnZXRfY29sbGVjdGlvbiJLChdBZGRHaXRIdWJTb3VyY2VSZXNwb25zZRIwCgpkYXRhc291cmNlGAEgASgLMhwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlIsgEChBBZGRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESQgoIcHJvdmlkZXIYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckIKukgHggEEEAEgABI6CgZnaXRodWIYBCABKAsyKC5zYWFzLmFjY291bnRzLnYxLkdpdEh1YkRhdGFzb3VyY2VDb25maWdIABI0CgNhcGkYBSABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWdIABI8CgdjcmF3bGVyGAggASgLMikuc2Fhcy5hY2NvdW50cy52MS5DcmF3bGVyRGF0YXNvdXJjZUNvbmZpZ0gAEjoKBnVwbG9hZBgJIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuVXBsb2FkRGF0YXNvdXJjZUNvbmZpZ0gAEiQKEGJvdW5kYXJ5X25vZGVfaWQYDCABKAlCCLpIBXIDsAEBSAESJgoQY29sbGVjdGlvbl9sYWJlbBgNIAEoCUIKukgHcgUQARj/AUgBEhwKCmNyZWRlbnRpYWwYBiABKAlCCLpIBXIDGIAgEiAKDndlYmhvb2tfc2VjcmV0GAcgASgJQgi6SAVyAxiACBImChRvYXV0aDJfY2xpZW50X3NlY3JldBgKIAEoCUIIukgFcgMYgAhCCAoGY29uZmlnQhEKCGJvdW5kYXJ5EgW6SAIIAUoECAMQBFIRdGFyZ2V0X2NvbGxlY3Rpb24iRQoRQWRkU291cmNlUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJaChVEYXRhc291cmNlQ29uZmlnRmllbGQSCwoDa2V5GAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIMCgRoZWxwGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIsQBChlEYXRhc291cmNlQ29ubmVjdG9yQnVkZ2V0EhoKEm1heF9pdGVtc19wZXJfY2FsbBgBIAEoDRIaChJtYXhfYnl0ZXNfcGVyX2NhbGwYAiABKAMSFgoObWF4X2l0ZW1fYnl0ZXMYAyABKAMSHQoVb3BlcmF0aW9uc19wZXJfd2luZG93GAQgASgNEhYKDndpbmRvd19zZWNvbmRzGAUgASgDEiAKGGJhY2tncm91bmRfc2hhcmVfcGVyY2VudBgGIAEoDSKqBgocRGF0YXNvdXJjZVByb3ZpZGVyRGVzY3JpcHRvchI2Cghwcm92aWRlchgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRI+Cg1jb25maWdfZmllbGRzGAQgAygLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ29uZmlnRmllbGQSGAoQc3VwcG9ydHNfd2ViaG9vaxgFIAEoCBJHChpzdXBwb3J0ZWRfY3JlZGVudGlhbF9raW5kcxgGIAMoDjIjLnNhYXMuYWNjb3VudHMudjEuQXBpQ3JlZGVudGlhbEtpbmQSEQoJY29ubmVjdG9yGAcgASgJEkEKCWludGVyZmFjZRgIIAEoDjIuLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRJEChBjcmVkZW50aWFsX21vZGVzGAkgAygOMiouc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSPwoNcmVhZGVyc19tb2RlbBgKIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVJlYWRlcnNNb2RlbBI7CgZidWRnZXQYCyABKAsyKy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VDb25uZWN0b3JCdWRnZXQSEgoKY29uZm9ybWFudBgMIAEoCBIXCg9jb25mb3JtYW5jZV9nYXAYDSABKAkSGwoTYWNjZXB0c19uZXdfc291cmNlcxgOIAEoCBIgChhsaXZlX2RlbGl2ZXJ5X2NvbmZpZ3VyZWQYDyABKAgSGgoSYWNjZXB0c19vcGVyYXRpb25zGBAgASgIEhYKDm9wZXJhdGlvbnNfZ2FwGBEgASgJEhsKE29hdXRoX2F1dGhvcml6ZV91cmwYEiABKAkSFwoPb2F1dGhfdG9rZW5fdXJsGBMgASgJEhQKDG9hdXRoX3Njb3BlcxgUIAMoCSIdChtHZXREYXRhc291cmNlQ2F0YWxvZ1JlcXVlc3QiYQocR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZRJBCglwcm92aWRlcnMYASADKAsyLi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckRlc2NyaXB0b3IiLgoSTGlzdFNvdXJjZXNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiSAoTTGlzdFNvdXJjZXNSZXNwb25zZRIxCgtkYXRhc291cmNlcxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJCChBHZXRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIkUKEUdldFNvdXJjZVJlc3BvbnNlEjAKCmRhdGFzb3VyY2UYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2UidgoRU3luY1NvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQESHgoMYWNjZXNzX3Rva2VuGAMgASgJQgi6SAVyAxiAIBIRCglyZWNvbm5lY3QYBCABKAgiJAoSU3luY1NvdXJjZVJlc3BvbnNlEg4KBmpvYl9pZBgBIAEoCSJqChRHZXRTb3VyY2VTeW5jUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoGam9iX2lkGAMgASgJQgu6SAjYAQFyA7ABASKVAQoSU291cmNlU3luY0RlbGl2ZXJ5EhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESLQoFc3RhdGUYAiABKA4yFi5zYWFzLmpvYnMudjEuSm9iU3RhdGVCBrpIA8gBARI2CglleGVjdXRpb24YAyABKAsyIy5zYWFzLmpvYnMudjEuSm9iRXhlY3V0aW9uUmVmZXJlbmNlIosBChFTb3VyY2VTeW5jQ2hhbmdlcxINCgVmaWxlcxgBIAEoDRINCgVhZGRlZBgCIAEoDRIQCghtb2RpZmllZBgDIAEoDRIPCgdkZWxldGVkGAQgASgNEhMKC3NwbGl0X2tub3duGAUgASgIEhAKCHNuYXBzaG90GAYgASgIEg4KBmNvbW1pdBgHIAEoCSKtAQoRU291cmNlU3luY0ZhaWx1cmUSOQoGcmVhc29uGAEgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jRmFpbHVyZVJlYXNvbhIMCgRjb2RlGAIgASgJEg8KB21lc3NhZ2UYAyABKAkSEAoIcmV0cnlpbmcYBCABKAgSLAoIcmV0cnlfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoQEChJTb3VyY2VTeW5jUHJvZ3Jlc3MSMAoFcGhhc2UYASABKA4yIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNQaGFzZRI0Cgd0cmlnZ2VyGAIgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jVHJpZ2dlchItCglxdWV1ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2ZldGNoaW5nX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtjb21waWxlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMQoNaGFuZGVkX29mZl9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZmluaXNoZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjQKB2NoYW5nZXMYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNDaGFuZ2VzEjQKB2ZhaWx1cmUYCSABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNGYWlsdXJlEg8KB2F0dGVtcHQYCiABKA0SFAoMbWF4X2F0dGVtcHRzGAsgASgNItIBChVHZXRTb3VyY2VTeW5jUmVzcG9uc2USGAoGam9iX2lkGAEgASgJQgi6SAVyA7ABARItCgVzdGF0ZRgCIAEoDjIWLnNhYXMuam9icy52MS5Kb2JTdGF0ZUIGukgDyAEBEjgKCmRlbGl2ZXJpZXMYAyADKAsyJC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNEZWxpdmVyeRI2Cghwcm9ncmVzcxgEIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1Byb2dyZXNzIkUKE0RlbGV0ZVNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiFgoURGVsZXRlU291cmNlUmVzcG9uc2UiVgoTR2l0SHViQXBwUmVwb3NpdG9yeRIMCgRyZXBvGAEgASgJEhYKDmRlZmF1bHRfYnJhbmNoGAIgASgJEhkKEWFscmVhZHlfY29ubmVjdGVkGAMgASgIIjYKGkJlZ2luR2l0SHViQXBwU2V0dXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEicQobQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlEhMKC2luc3RhbGxfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIpwBCh1Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGP8BEiwKD2luc3RhbGxhdGlvbl9pZBgDIAEoCUITukgQcg4QARggMgheWzAtOV0rJBIYCgRjb2RlGAQgASgJQgq6SAdyBRABGP8BInYKHkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSOwoMcmVwb3NpdG9yaWVzGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5HaXRIdWJBcHBSZXBvc2l0b3J5IlEKH01pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiVAogTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSLEAQoVRGF0YXNvdXJjZUFjY291bnRMaW5rEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRIPCgd1c2VyX2lkGAMgASgJEhEKCWNvbm5lY3RvchgEIAEoCRIbChNwcm92aWRlcl9hY2NvdW50X2lkGAUgASgJEh4KFnByb3ZpZGVyX2FjY291bnRfbG9naW4YBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAitwEKFkRhdGFzb3VyY2VHcm91cEJpbmRpbmcSCgoCaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhEKCWNvbm5lY3RvchgDIAEoCRIZChFwcm92aWRlcl9ncm91cF9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJEhIKCmNyZWF0ZWRfYnkYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilAIKGERhdGFzb3VyY2VWZXJpZmllZERvbWFpbhIKCgJpZBgBIAEoCRIOCgZvcmdfaWQYAiABKAkSDgoGZG9tYWluGAMgASgJEjgKBnN0YXR1cxgEIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURvbWFpblN0YXR1cxIXCg90eHRfcmVjb3JkX25hbWUYBSABKAkSGAoQdHh0X3JlY29yZF92YWx1ZRgGIAEoCRIvCgt2ZXJpZmllZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKY3JlYXRlZF9hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiMwoXRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCSKmAQohQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSHgoMcmVkaXJlY3RfdXJpGAMgASgJQgi6SAVyAxiAEBIRCglzb3VyY2VfaWQYBCABKAkiegoiQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRIVCg1hdXRob3JpemVfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInUKJENvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGIBAEhgKBGNvZGUYAyABKAlCCrpIB3IFEAEY/wEiXgolQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRI1CgRsaW5rGAEgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQWNjb3VudExpbmsiPwojTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJeCiRMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2USNgoFbGlua3MYASADKAsyJy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VBY2NvdW50TGluayJUCiJEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIiUKI0RlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIjkKHUdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiiwIKHkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXNwb25zZRI2CgVsaW5rcxgBIAMoCzInLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUFjY291bnRMaW5rEjoKCGJpbmRpbmdzGAIgAygLMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlR3JvdXBCaW5kaW5nEjsKB2RvbWFpbnMYAyADKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbhI4CgV0ZWFtcxgEIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0irgEKGkJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSJQoRcHJvdmlkZXJfZ3JvdXBfaWQYAyABKAlCCrpIB3IFEAEY/wESGQoHdGVhbV9pZBgEIAEoCUIIukgFcgOwAQEiWAobQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlEjkKB2JpbmRpbmcYASABKAsyKC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VHcm91cEJpbmRpbmciTgocVW5iaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIfCh1VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJUChxDbGFpbURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGgoGZG9tYWluGAIgASgJQgq6SAdyBRADGP0BIlsKHUNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlEjoKBmRvbWFpbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVZlcmlmaWVkRG9tYWluIk8KHVZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlwKHlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXNwb25zZRI6CgZkb21haW4YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbiJPCh1EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIgCh5EZWxldGVEYXRhc291cmNlRG9tYWluUmVzcG9uc2UingIKEFNvdXJjZURlbGVnYXRpb24SCgoCaWQYASABKAkSEQoJc291cmNlX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIOCgZtb2R1bGUYBCABKAkSDwoHYmluZGluZxgFIAEoCRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpyZXZva2VkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBJACgpyZXZvY2F0aW9uGAggASgOMiwuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhISCgpyZXZva2VkX2J5GAkgASgJIoEBChxMaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIAoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABAUgAiAEBEhcKD2luY2x1ZGVfcmV2b2tlZBgDIAEoCEIMCgpfc291cmNlX2lkIlgKHUxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlEjcKC2RlbGVnYXRpb25zGAEgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIk8KHVJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlgKHlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZRI2CgpkZWxlZ2F0aW9uGAEgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIjIKD1RyaWdnZXJJZGVudGl0eRIOCgZzb3VyY2UYASABKAkSDwoHYmluZGluZxgCIAEoCSKEAQoXRGVjbGFyZWRUcmlnZ2VyU2NoZWR1bGUSDAoEY3JvbhgBIAEoCRIQCgh0aW1lem9uZRgCIAEoCRISCgptaXNzZWRfcnVuGAMgASgJEg8KB292ZXJsYXAYBCABKAkSEAoIZGVhZGxpbmUYBSABKAMSEgoKaW5wdXRfanNvbhgGIAEoCSLnAgoaVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USEQoJcmVmZXJlbmNlGAEgASgJEhUKDWRlbGVnYXRpb25faWQYAiABKAkSDgoGbW9kdWxlGAMgASgJEg8KB2JpbmRpbmcYBCABKAkSEAoIYXVkaWVuY2UYBSABKAkSDgoGdGVuYW50GAYgASgJEhoKEm93bmVyX3ByaW5jaXBhbF9pZBgHIAEoCRIOCgZzY29wZXMYCCADKAkSDgoGYWN0aXZlGAkgASgIEi4KCmNyZWF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnJldm9rZWRfYXQYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEkAKCnJldm9jYXRpb24YDCABKA4yLC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uIr4BChNUcmlnZ2VyQ2hpbGRDb250ZXh0EhUKDWRlbGVnYXRpb25faWQYASABKAkSDgoGbW9kdWxlGAIgASgJEg8KB2JpbmRpbmcYAyABKAkSEAoIYXVkaWVuY2UYBCABKAkSDgoGc2NvcGVzGAUgAygJEg4KBmxvb2t1cBgGIAEoCBItCgltaW50ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBnJlY29yZBgIIAEoCSLNAgoYU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEg8KB3JlbGVhc2UYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEjsKCHNjaGVkdWxlGAMgASgLMikuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlZFRyaWdnZXJTY2hlZHVsZRIzCghpZGVudGl0eRgEIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlcklkZW50aXR5Eg4KBmRpZ2VzdBgFIAEoCRI2ChJsYXN0X29jY3VycmVuY2VfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjYKEm5leHRfb2NjdXJyZW5jZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTdW5kZWNsYXJhYmxlX3JlYXNvbhgIIAEoCSJWCh1EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQEiYwoeRGVsZWdhdGVTb3VyY2VUcmlnZ2Vyc1Jlc3BvbnNlEkEKC2RlbGVnYXRpb25zGAEgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5UcmlnZ2VyRGVsZWdhdGlvblJlZmVyZW5jZSJbCiJHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABASLiAQojR2V0U291cmNlVHJpZ2dlckRlY2xhcmF0aW9uUmVzcG9uc2USPwoLZGVjbGFyYXRpb24YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvbhJBCgtkZWxlZ2F0aW9ucxgCIAMoCzIsLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USNwoIY2hpbGRyZW4YAyADKAsyJS5zYWFzLmFjY291bnRzLnYxLlRyaWdnZXJDaGlsZENvbnRleHQibgoWUmVjb25jaWxlU291cmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESHQoJZWZmZWN0X2lkGAMgASgJQgq6SAdyBRABGIABIkwKF1JlY29uY2lsZVNvdXJjZVJlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIOCgZzdGF0dXMYAiABKAkSDgoGam9iX2lkGAMgASgJIj0KHExvb2t1cFJlY29uY2lsZVNvdXJjZVJlcXVlc3QSHQoJZWZmZWN0X2lkGAEgASgJQgq6SAdyBRABGIABIvECCg9Tb3VyY2VPcGVyYXRpb24SDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIOCgZtZXRob2QYAyABKAkSDAoEcGF0aBgEIAEoCRINCgVxdWVyeRgFIAMoCRItCgxpbnB1dF9zY2hlbWEYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0Ei4KDW91dHB1dF9zY2hlbWEYByABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EjgKBmVmZmVjdBgIIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uLkVmZmVjdBIYChBtYXhfb3V0cHV0X2J5dGVzGAkgASgNEg4KBmRpZ2VzdBgKIAEoCSJLCgZFZmZlY3QSFgoSRUZGRUNUX1VOU1BFQ0lGSUVEEAASFAoQRUZGRUNUX1JFQURfT05MWRABEhMKD0VGRkVDVF9NVVRBVElPThACIo4BCh5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEjUKCm9wZXJhdGlvbnMYAyADKAsyIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvbiJYCh9EZWNsYXJlU291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlEjUKCm9wZXJhdGlvbnMYASADKAsyIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvbiJUChtMaXN0U291cmNlT3BlcmF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBIlUKHExpc3RTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2USNQoKb3BlcmF0aW9ucxgBIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIsMBChxJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABARIwCglvcGVyYXRpb24YAyABKAlCHbpIGnIYMhZeW2Etel1bYS16MC05X117MCw2M30kEh0KCmlucHV0X2pzb24YBCABKAlCCbpIBnIEKICABBIbCgllZmZlY3RfaWQYBSABKAlCCLpIBXIDKIABIn8KFlNvdXJjZU9wZXJhdGlvblJlY2VpcHQSEQoJZWZmZWN0X2lkGAEgASgJEhQKDGNvbW1pdHRlZF9hdBgCIAEoCRIOCgZzdGF0dXMYAyABKAkSFwoPcHJvdmlkZXJfc3RhdHVzGAQgASgNEhMKC291dHB1dF9qc29uGAUgASgJIm8KHUludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlEhMKC291dHB1dF9qc29uGAEgASgJEjkKB3JlY2VpcHQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvblJlY2VpcHQiQwoiTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEogAEieQojUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEhsKCWVmZmVjdF9pZBgDIAEoCUIIukgFcgMogAEiWwokUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIQCghyZWNlaXB0cxgCIAEoAxIOCgZzdGF0dXMYAyABKAkiSgopTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QSHQoJZWZmZWN0X2lkGAEgASgJQgq6SAdyBRABKIABKrcBChJEYXRhc291cmNlUHJvdmlkZXISIwofREFUQVNPVVJDRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEh4KGkRBVEFTT1VSQ0VfUFJPVklERVJfR0lUSFVCEAESGwoXREFUQVNPVVJDRV9QUk9WSURFUl9BUEkQAhIfChtEQVRBU09VUkNFX1BST1ZJREVSX0NSQVdMRVIQAxIeChpEQVRBU09VUkNFX1BST1ZJREVSX1VQTE9BRBAEKpEBChBEYXRhc291cmNlU3RhdHVzEiEKHURBVEFTT1VSQ0VfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHAoYREFUQVNPVVJDRV9TVEFUVVNfQUNUSVZFEAESHAoYREFUQVNPVVJDRV9TVEFUVVNfUEFVU0VEEAISHgoaREFUQVNPVVJDRV9TVEFUVVNfREVHUkFERUQQAyq8AQoWRGF0YXNvdXJjZUxpdmVEZWxpdmVyeRIoCiREQVRBU09VUkNFX0xJVkVfREVMSVZFUllfVU5TUEVDSUZJRUQQABIhCh1EQVRBU09VUkNFX0xJVkVfREVMSVZFUllfTk9ORRABEisKJ0RBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9TT1VSQ0VfV0VCSE9PSxACEigKJERBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9BUFBfV0VCSE9PSxADKtYBChFBcGlDcmVkZW50aWFsS2luZBIjCh9BUElfQ1JFREVOVElBTF9LSU5EX1VOU1BFQ0lGSUVEEAASHgoaQVBJX0NSRURFTlRJQUxfS0lORF9CRUFSRVIQARIdChlBUElfQ1JFREVOVElBTF9LSU5EX0JBU0lDEAISHgoaQVBJX0NSRURFTlRJQUxfS0lORF9IRUFERVIQAxIdChlBUElfQ1JFREVOVElBTF9LSU5EX1FVRVJZEAQSHgoaQVBJX0NSRURFTlRJQUxfS0lORF9PQVVUSDIQBSqmAgocRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRIuCipEQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfVU5TUEVDSUZJRUQQABIoCiREQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfRklMRVMQARIoCiREQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfUEFHRVMQAhIqCiZEQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfUkVDT1JEUxADEisKJ0RBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9NRVNTQUdFUxAEEikKJURBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9FVkVOVFMQBSrsAQoYRGF0YXNvdXJjZUNyZWRlbnRpYWxNb2RlEioKJkRBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX1VOU1BFQ0lGSUVEEAASIwofREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfTk9ORRABEiYKIkRBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX09SR19BUFAQAhIpCiVEQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9VU0VSX09BVVRIEAMSLAooREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfU1RBVElDX1NFQ1JFVBAEKpcBChZEYXRhc291cmNlUmVhZGVyc01vZGVsEigKJERBVEFTT1VSQ0VfUkVBREVSU19NT0RFTF9VTlNQRUNJRklFRBAAEioKJkRBVEFTT1VSQ0VfUkVBREVSU19NT0RFTF9TT1VSQ0VfU0NPUEVEEAESJwojREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1RSQU5TTEFURUQQAiruAQoPU291cmNlU3luY1BoYXNlEiEKHVNPVVJDRV9TWU5DX1BIQVNFX1VOU1BFQ0lGSUVEEAASHAoYU09VUkNFX1NZTkNfUEhBU0VfUVVFVUVEEAESHgoaU09VUkNFX1NZTkNfUEhBU0VfRkVUQ0hJTkcQAhIeChpTT1VSQ0VfU1lOQ19QSEFTRV9DT01QSUxFRBADEiAKHFNPVVJDRV9TWU5DX1BIQVNFX0hBTkRFRF9PRkYQBBIaChZTT1VSQ0VfU1lOQ19QSEFTRV9ET05FEAUSHAoYU09VUkNFX1NZTkNfUEhBU0VfRkFJTEVEEAYqpgMKF1NvdXJjZVN5bmNGYWlsdXJlUmVhc29uEioKJlNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX1VOU1BFQ0lGSUVEEAASKwonU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fUkFURV9MSU1JVEVEEAESKQolU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fQ1JFREVOVElBTBACEiwKKFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0FDQ0VTU19ERU5JRUQQAxIoCiRTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9OT1RfRk9VTkQQBBIoCiRTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9UT09fTEFSR0UQBRIvCitTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9IT1NUX1VOQVZBSUxBQkxFEAYSJAogU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fT1RIRVIQBxIuCipTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9ERUxJVkVSWV9GQUlMRUQQCCqcAQoRU291cmNlU3luY1RyaWdnZXISIwofU09VUkNFX1NZTkNfVFJJR0dFUl9VTlNQRUNJRklFRBAAEh4KGlNPVVJDRV9TWU5DX1RSSUdHRVJfTUFOVUFMEAESIQodU09VUkNFX1NZTkNfVFJJR0dFUl9TQ0hFRFVMRUQQAhIfChtTT1VSQ0VfU1lOQ19UUklHR0VSX1dFQkhPT0sQAyqPAQoWRGF0YXNvdXJjZURvbWFpblN0YXR1cxIoCiREQVRBU09VUkNFX0RPTUFJTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIkCiBEQVRBU09VUkNFX0RPTUFJTl9TVEFUVVNfUEVORElORxABEiUKIURBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19WRVJJRklFRBACKpUDChpTb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhIsCihTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1VOU1BFQ0lGSUVEEAASKAokU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9SRVZPS0VEEAESKQolU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9SRVBMQUNFRBACEi8KK1NPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fU09VUkNFX0RFTEVURUQQAxIvCitTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX01FTUJFUl9SRU1PVkVEEAQSMAosU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9QRVJNSVNTSU9OX0xPU1QQBRIuCipTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1VTRVJfSU5BQ1RJVkUQBhIwCixTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX0JJTkRJTkdfQ0hBTkdFRBAHMok4ChFEYXRhc291cmNlU2VydmljZRK5AwocUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0cxI1LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLlBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNSZXNwb25zZSKpAsLzGGwIAhADGhFkYXRhc291cmNlOmludm9rZSIRZGF0YXNvdXJjZTppbnZva2UqDwoJc291cmNlX2lkEAQYAzABOiMKH3NhYXMuZGF0YXNvdXJjZS5yZWNlaXB0cy5wcnVuZWQQAkADSARQA1gDYAHa8xi0AQoCCDwSAwjYBBgFIgIIAioLVU5BVkFJTEFCTEUqElJFU09VUkNFX0VYSEFVU1RFRCoHQUJPUlRFRDIPc2Fhcy1kYXRhc291cmNlSkYvc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlU2VydmljZS9Mb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUIAgWIAgYAFyGAoGc291cmNlEgZpbnZva2USBHJlYWQYARLmAQoiTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0cxI7LnNhYXMuYWNjb3VudHMudjEuTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLlBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNSZXNwb25zZSJLwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUANYA2ABEvwDChVJbnZva2VTb3VyY2VPcGVyYXRpb24SLi5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlIoEDwvMYbggCEAMaEWRhdGFzb3VyY2U6aW52b2tlIhFkYXRhc291cmNlOmludm9rZSoPCglzb3VyY2VfaWQQBBgDMAE6JQohc2Fhcy5kYXRhc291cmNlLm9wZXJhdGlvbi5pbnZva2VkEARAA0gEUARYBGAB2vMYrwEKAgg8EgMI2AQYBSICCAIqC1VOQVZBSUxBQkxFKhJSRVNPVVJDRV9FWEhBVVNURUQqB0FCT1JURUQyD3NhYXMtZGF0YXNvdXJjZUo/L3NhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVNlcnZpY2UvTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uUICABFiAgEBgAXIYCgZzb3VyY2USBmludm9rZRIEcmVhZBgBgtPkkwJVOgEqIlAvdjEvb3JnYW5pemF0aW9ucy97b3JnX2lkfS9kYXRhc291cmNlcy97c291cmNlX2lkfS9vcGVyYXRpb25zL3tvcGVyYXRpb259Omludm9rZRKHAgobTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uEjQuc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5JbnZva2VTb3VyY2VPcGVyYXRpb25SZXNwb25zZSKAAcLzGEcIAhADGg9kYXRhc291cmNlOnJlYWQiD2RhdGFzb3VyY2U6cmVhZCoPCgllZmZlY3RfaWQQBBgDMAE6AhABQAFIA1ADWARgAYLT5JMCLxItL3YxL2RhdGFzb3VyY2Utb3BlcmF0aW9uLXJlY2VpcHRzL3tlZmZlY3RfaWR9EoUCChRMaXN0U291cmNlT3BlcmF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdFNvdXJjZU9wZXJhdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlIo0BwvMYRAgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwI/Ej0vdjEvb3JnYW5pemF0aW9ucy97b3JnX2lkfS9kYXRhc291cmNlcy97c291cmNlX2lkfS9vcGVyYXRpb25zEpMCChdEZWNsYXJlU291cmNlT3BlcmF0aW9ucxIwLnNhYXMuYWNjb3VudHMudjEuRGVjbGFyZVNvdXJjZU9wZXJhdGlvbnNSZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlIpIBwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5vcGVyYXRpb24uZGVjbGFyZWQQAkABSARQA1gDYAGC0+STAkI6ASoiPS92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMSrAEKD0FkZEdpdEh1YlNvdXJjZRIoLnNhYXMuYWNjb3VudHMudjEuQWRkR2l0SHViU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuQWRkR2l0SHViU291cmNlUmVzcG9uc2UiRMLzGEAIAhAEKgwKBm9yZ19pZBACGAEwATogChxzYWFzLmRhdGFzb3VyY2Uuc291cmNlLmFkZGVkEAJAAUgEUARYA2ABEpoBCglBZGRTb3VyY2USIi5zYWFzLmFjY291bnRzLnYxLkFkZFNvdXJjZVJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLkFkZFNvdXJjZVJlc3BvbnNlIkTC8xhACAIQBCoMCgZvcmdfaWQQAhgBMAE6IAocc2Fhcy5kYXRhc291cmNlLnNvdXJjZS5hZGRlZBACQAFIBFAEWANgARKPAQoUR2V0RGF0YXNvdXJjZUNhdGFsb2cSLS5zYWFzLmFjY291bnRzLnYxLkdldERhdGFzb3VyY2VDYXRhbG9nUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgDUAJYAmABEoIBCgtMaXN0U291cmNlcxIkLnNhYXMuYWNjb3VudHMudjEuTGlzdFNvdXJjZXNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlc1Jlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARJ8CglHZXRTb3VyY2USIi5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVJlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKeAQoKU3luY1NvdXJjZRIjLnNhYXMuYWNjb3VudHMudjEuU3luY1NvdXJjZVJlcXVlc3QaJC5zYWFzLmFjY291bnRzLnYxLlN5bmNTb3VyY2VSZXNwb25zZSJFwvMYQQgCEAQqDAoGb3JnX2lkEAIYATABOiEKHXNhYXMuZGF0YXNvdXJjZS5zb3VyY2Uuc3luY2VkEAJAAUgEUARYAmABEogBCg1HZXRTb3VyY2VTeW5jEiYuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VTeW5jUmVxdWVzdBonLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlU3luY1Jlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKlAQoMRGVsZXRlU291cmNlEiUuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVTb3VyY2VSZXF1ZXN0GiYuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVTb3VyY2VSZXNwb25zZSJGwvMYQggCEAQqDAoGb3JnX2lkEAIYATABOiIKHnNhYXMuZGF0YXNvdXJjZS5zb3VyY2UucmVtb3ZlZBACQAFIBFACWAJgARLEAQoTQmVnaW5HaXRIdWJBcHBTZXR1cBIsLnNhYXMuYWNjb3VudHMudjEuQmVnaW5HaXRIdWJBcHBTZXR1cFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkJlZ2luR2l0SHViQXBwU2V0dXBSZXNwb25zZSJQwvMYTAgCEAQqDAoGb3JnX2lkEAIYATABOiwKKHNhYXMuZGF0YXNvdXJjZS5naXRodWJfYXBwLnNldHVwX3N0YXJ0ZWQQAkABSARQAlgEYAESzwEKFkNvbXBsZXRlR2l0SHViQXBwU2V0dXASLy5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVzcG9uc2UiUsLzGE4IAhAEKgwKBm9yZ19pZBACGAEwATouCipzYWFzLmRhdGFzb3VyY2UuZ2l0aHViX2FwcC5zZXR1cF9jb21wbGV0ZWQQAkABSARQBFgDYAESzQEKGE1pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcBIxLnNhYXMuYWNjb3VudHMudjEuTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVxdWVzdBoyLnNhYXMuYWNjb3VudHMudjEuTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2UiSsLzGEYIAhAEKgwKBm9yZ19pZBACGAEwATomCiJzYWFzLmRhdGFzb3VyY2UuY3JlZGVudGlhbC51cGRhdGVkEAJAAUgEUAJYA2ABEtUBChpCZWdpbkRhdGFzb3VyY2VBY2NvdW50TGluaxIzLnNhYXMuYWNjb3VudHMudjEuQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0GjQuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkRhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkzC8xhICAIQAyoMCgZvcmdfaWQQAhgBMAE6KAokc2Fhcy5kYXRhc291cmNlLmFjY291bnRfbGlua19zdGFydGVkEAJAAUgEUAJYBGABEtgBCh1Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGluaxI2LnNhYXMuYWNjb3VudHMudjEuQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkbC8xhCCAIQAyoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmFjY291bnRfbGlua2VkEAJAAUgEUARYA2ABErUBChxMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzEjUuc2Fhcy5hY2NvdW50cy52MS5MaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1Jlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARLUAQobRGVsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rEjQuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZSJIwvMYRAgCEAMqDAoGb3JnX2lkEAIYATABOiQKIHNhYXMuZGF0YXNvdXJjZS5hY2NvdW50X3VubGlua2VkEAJAAUgEUAJYA2ABEqMBChZHZXREYXRhc291cmNlRGlyZWN0b3J5Ei8uc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlRGlyZWN0b3J5UmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZURpcmVjdG9yeVJlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARK3AQoTQmluZERhdGFzb3VyY2VHcm91cBIsLnNhYXMuYWNjb3VudHMudjEuQmluZERhdGFzb3VyY2VHcm91cFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJDwvMYPwgCEAQqDAoGb3JnX2lkEAIYATABOh8KG3NhYXMuZGF0YXNvdXJjZS5ncm91cF9ib3VuZBACQAFIBFACWANgARK/AQoVVW5iaW5kRGF0YXNvdXJjZUdyb3VwEi4uc2Fhcy5hY2NvdW50cy52MS5VbmJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJFwvMYQQgCEAQqDAoGb3JnX2lkEAIYATABOiEKHXNhYXMuZGF0YXNvdXJjZS5ncm91cF91bmJvdW5kEAJAAUgEUAJYA2ABEsABChVDbGFpbURhdGFzb3VyY2VEb21haW4SLi5zYWFzLmFjY291bnRzLnYxLkNsYWltRGF0YXNvdXJjZURvbWFpblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmRvbWFpbl9jbGFpbWVkEAJAAUgEUAJYA2ABEsQBChZWZXJpZnlEYXRhc291cmNlRG9tYWluEi8uc2Fhcy5hY2NvdW50cy52MS5WZXJpZnlEYXRhc291cmNlRG9tYWluUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuVmVyaWZ5RGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkfC8xhDCAIQBCoMCgZvcmdfaWQQAhgBMAE6Iwofc2Fhcy5kYXRhc291cmNlLmRvbWFpbl92ZXJpZmllZBACQAFIBFACWANgARLDAQoWRGVsZXRlRGF0YXNvdXJjZURvbWFpbhIvLnNhYXMuYWNjb3VudHMudjEuRGVsZXRlRGF0YXNvdXJjZURvbWFpblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VEb21haW5SZXNwb25zZSJGwvMYQggCEAQqDAoGb3JnX2lkEAIYATABOiIKHnNhYXMuZGF0YXNvdXJjZS5kb21haW5fcmVtb3ZlZBACQAFIBFACWANgARKgAQoVTGlzdFNvdXJjZURlbGVnYXRpb25zEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlRGVsZWdhdGlvbnNSZXNwb25zZSImwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQAlgDYAESxwEKFlJldm9rZVNvdXJjZURlbGVnYXRpb24SLy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTb3VyY2VEZWxlZ2F0aW9uUmVzcG9uc2UiSsLzGEYIAhAEKgwKBm9yZ19pZBACGAEwATomCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5yZXZva2VkEAJAAUgEUAJYA2ABEusBChZEZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzEi8uc2Fhcy5hY2NvdW50cy52MS5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuRGVsZWdhdGVTb3VyY2VUcmlnZ2Vyc1Jlc3BvbnNlIm7C8xhqCAIQBCoMCgZvcmdfaWQQAhgBMAE6Sgoic2Fhcy5kYXRhc291cmNlLmRlbGVnYXRpb24uY3JlYXRlZAoic2Fhcy5kYXRhc291cmNlLmRlbGVnYXRpb24ucmV2b2tlZBACQAFIBFACWANgARKyAQobR2V0U291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEjQuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXNwb25zZSImwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQAlgDYAESgwMKD1JlY29uY2lsZVNvdXJjZRIoLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVzcG9uc2UimgLC8xhqCAIQAxoRZGF0YXNvdXJjZTppbnZva2UiEWRhdGFzb3VyY2U6aW52b2tlKg8KCXNvdXJjZV9pZBAEGAMwATohCh1zYWFzLmRhdGFzb3VyY2Uuc291cmNlLnN5bmNlZBACQANIBFACWAJgAdrzGKcBCgIIPBIDCNgEGAUiAggCKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMg9zYWFzLWRhdGFzb3VyY2VKOS9zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTZXJ2aWNlL0xvb2t1cFJlY29uY2lsZVNvdXJjZVCAIFiAIGABchgKBnNvdXJjZRIGaW52b2tlEgRyZWFkGAESvwEKFUxvb2t1cFJlY29uY2lsZVNvdXJjZRIuLnNhYXMuYWNjb3VudHMudjEuTG9va3VwUmVjb25jaWxlU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVzcG9uc2UiS8LzGEcIAhADGg9kYXRhc291cmNlOnJlYWQiD2RhdGFzb3VyY2U6cmVhZCoPCgllZmZlY3RfaWQQBBgDMAE6AhABQAFIA1ACWAJgAUK3AQoUY29tLnNhYXMuYWNjb3VudHMudjFCD0RhdGFzb3VyY2VQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_codefly_runnable_v0_options, file_google_api_annotations, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiFzYWFzL2FjY291bnRzL3YxL2RhdGFzb3VyY2UucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoWR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIMCgRyZXBvGAEgASgJEg0KBXBhdGhzGAIgAygJEg4KBmJyYW5jaBgDIAEoCRIXCg9maWxlX2V4dGVuc2lvbnMYBCADKAki9AEKD0FwaU9BdXRoMkNvbmZpZxI2CgVncmFudBgEIAEoDjInLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnLkdyYW50EhEKCXRva2VuX3VybBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSDgoGc2NvcGVzGAMgAygJInMKBUdyYW50EhUKEUdSQU5UX1VOU1BFQ0lGSUVEEAASFwoTR1JBTlRfUkVGUkVTSF9UT0tFThABEhwKGEdSQU5UX0NMSUVOVF9DUkVERU5USUFMUxACEhwKGEdSQU5UX0FVVEhPUklaQVRJT05fQ09ERRADIuoBChNBcGlEYXRhc291cmNlQ29uZmlnEhAKCGJhc2VfdXJsGAEgASgJEhUKDXJlc291cmNlX3BhdGgYAiABKAkSPAoPY3JlZGVudGlhbF9raW5kGAMgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5BcGlDcmVkZW50aWFsS2luZBIZChFjcmVkZW50aWFsX2hlYWRlchgEIAEoCRIeChZjcmVkZW50aWFsX3F1ZXJ5X3BhcmFtGAUgASgJEjEKBm9hdXRoMhgGIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnIkEKF0NyYXdsZXJEYXRhc291cmNlQ29uZmlnEhMKC3NpdGVtYXBfdXJsGAEgASgJEhEKCW1heF9wYWdlcxgCIAEoDSKGAQoWVXBsb2FkRGF0YXNvdXJjZUNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIOCgZyZWdpb24YAiABKAkSDgoGYnVja2V0GAMgASgJEg4KBnByZWZpeBgEIAEoCRIVCg1hY2Nlc3Nfa2V5X2lkGAUgASgJEhMKC21heF9vYmplY3RzGAYgASgNIoMHCgpEYXRhc291cmNlEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRI2Cghwcm92aWRlchgDIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEjgKBmdpdGh1YhgFIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIyCgZzdGF0dXMYBiABKA4yIi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTdGF0dXMSGgoSd2ViaG9va19jb25maWd1cmVkGAcgASgIEi4KCmNyZWF0ZWRfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDmxhc3Rfc3luY2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCgNhcGkYCyABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWcSOgoHY3Jhd2xlchgMIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuQ3Jhd2xlckRhdGFzb3VyY2VDb25maWcSOAoGdXBsb2FkGA0gASgLMiguc2Fhcy5hY2NvdW50cy52MS5VcGxvYWREYXRhc291cmNlQ29uZmlnEhgKEGJvdW5kYXJ5X25vZGVfaWQYDiABKAkSNAoQbGFzdF9pbmdlc3RlZF9hdBgPIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoUbGFzdF9pbmdlc3RlZF9jb21taXQYECABKAkSFQoNc3RhdHVzX3JlYXNvbhgRIAEoCRIWCg5ib3VuZGFyeV9sYWJlbBgSIAEoCRISCgpjb25mb3JtYW50GBMgASgIEhcKD2NvbmZvcm1hbmNlX2dhcBgUIAEoCRI/Cg1saXZlX2RlbGl2ZXJ5GBUgASgOMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlTGl2ZURlbGl2ZXJ5EjUKEnJlY29uY2lsZV9pbnRlcnZhbBgWIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkoECAQQBVIRdGFyZ2V0X2NvbGxlY3Rpb24isgMKFkFkZEdpdEh1YlNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI7CgRyZXBvGAIgASgJQi26SCpyKBADGP8BMiFeW0EtWmEtejAtOV8uLV0rL1tBLVphLXowLTlfLi1dKyQSIAoFcGF0aHMYAyADKAlCEbpIDpIBCxBAIgdyBRABGIAEEhgKBmJyYW5jaBgEIAEoCUIIukgFcgMY/wESJAoQYm91bmRhcnlfbm9kZV9pZBgIIAEoCUIIukgFcgOwAQFIABImChBjb2xsZWN0aW9uX2xhYmVsGAkgASgJQgq6SAdyBRABGP8BSAASHgoMYWNjZXNzX3Rva2VuGAYgASgJQgi6SAVyAxiACBIgCg53ZWJob29rX3NlY3JldBgHIAEoCUIIukgFcgMYgAgSSQoPZmlsZV9leHRlbnNpb25zGAogAygJQjC6SC2SASoQICImciQQAhghMh5eXC5bQS1aYS16MC05XVtBLVphLXowLTkuXy1dKiRCEQoIYm91bmRhcnkSBbpIAggBSgQIBRAGUhF0YXJnZXRfY29sbGVjdGlvbiJLChdBZGRHaXRIdWJTb3VyY2VSZXNwb25zZRIwCgpkYXRhc291cmNlGAEgASgLMhwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlIsgEChBBZGRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESQgoIcHJvdmlkZXIYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckIKukgHggEEEAEgABI6CgZnaXRodWIYBCABKAsyKC5zYWFzLmFjY291bnRzLnYxLkdpdEh1YkRhdGFzb3VyY2VDb25maWdIABI0CgNhcGkYBSABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWdIABI8CgdjcmF3bGVyGAggASgLMikuc2Fhcy5hY2NvdW50cy52MS5DcmF3bGVyRGF0YXNvdXJjZUNvbmZpZ0gAEjoKBnVwbG9hZBgJIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuVXBsb2FkRGF0YXNvdXJjZUNvbmZpZ0gAEiQKEGJvdW5kYXJ5X25vZGVfaWQYDCABKAlCCLpIBXIDsAEBSAESJgoQY29sbGVjdGlvbl9sYWJlbBgNIAEoCUIKukgHcgUQARj/AUgBEhwKCmNyZWRlbnRpYWwYBiABKAlCCLpIBXIDGIAgEiAKDndlYmhvb2tfc2VjcmV0GAcgASgJQgi6SAVyAxiACBImChRvYXV0aDJfY2xpZW50X3NlY3JldBgKIAEoCUIIukgFcgMYgAhCCAoGY29uZmlnQhEKCGJvdW5kYXJ5EgW6SAIIAUoECAMQBFIRdGFyZ2V0X2NvbGxlY3Rpb24iRQoRQWRkU291cmNlUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJaChVEYXRhc291cmNlQ29uZmlnRmllbGQSCwoDa2V5GAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIMCgRoZWxwGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIsQBChlEYXRhc291cmNlQ29ubmVjdG9yQnVkZ2V0EhoKEm1heF9pdGVtc19wZXJfY2FsbBgBIAEoDRIaChJtYXhfYnl0ZXNfcGVyX2NhbGwYAiABKAMSFgoObWF4X2l0ZW1fYnl0ZXMYAyABKAMSHQoVb3BlcmF0aW9uc19wZXJfd2luZG93GAQgASgNEhYKDndpbmRvd19zZWNvbmRzGAUgASgDEiAKGGJhY2tncm91bmRfc2hhcmVfcGVyY2VudBgGIAEoDSKqBgocRGF0YXNvdXJjZVByb3ZpZGVyRGVzY3JpcHRvchI2Cghwcm92aWRlchgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRI+Cg1jb25maWdfZmllbGRzGAQgAygLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ29uZmlnRmllbGQSGAoQc3VwcG9ydHNfd2ViaG9vaxgFIAEoCBJHChpzdXBwb3J0ZWRfY3JlZGVudGlhbF9raW5kcxgGIAMoDjIjLnNhYXMuYWNjb3VudHMudjEuQXBpQ3JlZGVudGlhbEtpbmQSEQoJY29ubmVjdG9yGAcgASgJEkEKCWludGVyZmFjZRgIIAEoDjIuLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRJEChBjcmVkZW50aWFsX21vZGVzGAkgAygOMiouc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSPwoNcmVhZGVyc19tb2RlbBgKIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVJlYWRlcnNNb2RlbBI7CgZidWRnZXQYCyABKAsyKy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VDb25uZWN0b3JCdWRnZXQSEgoKY29uZm9ybWFudBgMIAEoCBIXCg9jb25mb3JtYW5jZV9nYXAYDSABKAkSGwoTYWNjZXB0c19uZXdfc291cmNlcxgOIAEoCBIgChhsaXZlX2RlbGl2ZXJ5X2NvbmZpZ3VyZWQYDyABKAgSGgoSYWNjZXB0c19vcGVyYXRpb25zGBAgASgIEhYKDm9wZXJhdGlvbnNfZ2FwGBEgASgJEhsKE29hdXRoX2F1dGhvcml6ZV91cmwYEiABKAkSFwoPb2F1dGhfdG9rZW5fdXJsGBMgASgJEhQKDG9hdXRoX3Njb3BlcxgUIAMoCSIdChtHZXREYXRhc291cmNlQ2F0YWxvZ1JlcXVlc3QiYQocR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZRJBCglwcm92aWRlcnMYASADKAsyLi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckRlc2NyaXB0b3IiLgoSTGlzdFNvdXJjZXNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiSAoTTGlzdFNvdXJjZXNSZXNwb25zZRIxCgtkYXRhc291cmNlcxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJCChBHZXRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIkUKEUdldFNvdXJjZVJlc3BvbnNlEjAKCmRhdGFzb3VyY2UYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2UidgoRU3luY1NvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQESHgoMYWNjZXNzX3Rva2VuGAMgASgJQgi6SAVyAxiAIBIRCglyZWNvbm5lY3QYBCABKAgiJAoSU3luY1NvdXJjZVJlc3BvbnNlEg4KBmpvYl9pZBgBIAEoCSJqChRHZXRTb3VyY2VTeW5jUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoGam9iX2lkGAMgASgJQgu6SAjYAQFyA7ABASKVAQoSU291cmNlU3luY0RlbGl2ZXJ5EhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESLQoFc3RhdGUYAiABKA4yFi5zYWFzLmpvYnMudjEuSm9iU3RhdGVCBrpIA8gBARI2CglleGVjdXRpb24YAyABKAsyIy5zYWFzLmpvYnMudjEuSm9iRXhlY3V0aW9uUmVmZXJlbmNlIosBChFTb3VyY2VTeW5jQ2hhbmdlcxINCgVmaWxlcxgBIAEoDRINCgVhZGRlZBgCIAEoDRIQCghtb2RpZmllZBgDIAEoDRIPCgdkZWxldGVkGAQgASgNEhMKC3NwbGl0X2tub3duGAUgASgIEhAKCHNuYXBzaG90GAYgASgIEg4KBmNvbW1pdBgHIAEoCSKtAQoRU291cmNlU3luY0ZhaWx1cmUSOQoGcmVhc29uGAEgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jRmFpbHVyZVJlYXNvbhIMCgRjb2RlGAIgASgJEg8KB21lc3NhZ2UYAyABKAkSEAoIcmV0cnlpbmcYBCABKAgSLAoIcmV0cnlfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoQEChJTb3VyY2VTeW5jUHJvZ3Jlc3MSMAoFcGhhc2UYASABKA4yIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNQaGFzZRI0Cgd0cmlnZ2VyGAIgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jVHJpZ2dlchItCglxdWV1ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2ZldGNoaW5nX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtjb21waWxlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMQoNaGFuZGVkX29mZl9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZmluaXNoZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjQKB2NoYW5nZXMYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNDaGFuZ2VzEjQKB2ZhaWx1cmUYCSABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNGYWlsdXJlEg8KB2F0dGVtcHQYCiABKA0SFAoMbWF4X2F0dGVtcHRzGAsgASgNItIBChVHZXRTb3VyY2VTeW5jUmVzcG9uc2USGAoGam9iX2lkGAEgASgJQgi6SAVyA7ABARItCgVzdGF0ZRgCIAEoDjIWLnNhYXMuam9icy52MS5Kb2JTdGF0ZUIGukgDyAEBEjgKCmRlbGl2ZXJpZXMYAyADKAsyJC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNEZWxpdmVyeRI2Cghwcm9ncmVzcxgEIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1Byb2dyZXNzIkUKE0RlbGV0ZVNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiFgoURGVsZXRlU291cmNlUmVzcG9uc2UiVgoTR2l0SHViQXBwUmVwb3NpdG9yeRIMCgRyZXBvGAEgASgJEhYKDmRlZmF1bHRfYnJhbmNoGAIgASgJEhkKEWFscmVhZHlfY29ubmVjdGVkGAMgASgIIjYKGkJlZ2luR2l0SHViQXBwU2V0dXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEicQobQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlEhMKC2luc3RhbGxfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIpwBCh1Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGP8BEiwKD2luc3RhbGxhdGlvbl9pZBgDIAEoCUITukgQcg4QARggMgheWzAtOV0rJBIYCgRjb2RlGAQgASgJQgq6SAdyBRABGP8BInYKHkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSOwoMcmVwb3NpdG9yaWVzGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5HaXRIdWJBcHBSZXBvc2l0b3J5IlEKH01pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiVAogTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSLEAQoVRGF0YXNvdXJjZUFjY291bnRMaW5rEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRIPCgd1c2VyX2lkGAMgASgJEhEKCWNvbm5lY3RvchgEIAEoCRIbChNwcm92aWRlcl9hY2NvdW50X2lkGAUgASgJEh4KFnByb3ZpZGVyX2FjY291bnRfbG9naW4YBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAitwEKFkRhdGFzb3VyY2VHcm91cEJpbmRpbmcSCgoCaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhEKCWNvbm5lY3RvchgDIAEoCRIZChFwcm92aWRlcl9ncm91cF9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJEhIKCmNyZWF0ZWRfYnkYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilAIKGERhdGFzb3VyY2VWZXJpZmllZERvbWFpbhIKCgJpZBgBIAEoCRIOCgZvcmdfaWQYAiABKAkSDgoGZG9tYWluGAMgASgJEjgKBnN0YXR1cxgEIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURvbWFpblN0YXR1cxIXCg90eHRfcmVjb3JkX25hbWUYBSABKAkSGAoQdHh0X3JlY29yZF92YWx1ZRgGIAEoCRIvCgt2ZXJpZmllZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKY3JlYXRlZF9hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiMwoXRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCSKmAQohQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSHgoMcmVkaXJlY3RfdXJpGAMgASgJQgi6SAVyAxiAEBIRCglzb3VyY2VfaWQYBCABKAkiegoiQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRIVCg1hdXRob3JpemVfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInUKJENvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGIBAEhgKBGNvZGUYAyABKAlCCrpIB3IFEAEY/wEiXgolQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRI1CgRsaW5rGAEgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQWNjb3VudExpbmsiPwojTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJeCiRMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2USNgoFbGlua3MYASADKAsyJy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VBY2NvdW50TGluayJUCiJEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIiUKI0RlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIjkKHUdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiiwIKHkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXNwb25zZRI2CgVsaW5rcxgBIAMoCzInLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUFjY291bnRMaW5rEjoKCGJpbmRpbmdzGAIgAygLMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlR3JvdXBCaW5kaW5nEjsKB2RvbWFpbnMYAyADKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbhI4CgV0ZWFtcxgEIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0irgEKGkJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSJQoRcHJvdmlkZXJfZ3JvdXBfaWQYAyABKAlCCrpIB3IFEAEY/wESGQoHdGVhbV9pZBgEIAEoCUIIukgFcgOwAQEiWAobQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlEjkKB2JpbmRpbmcYASABKAsyKC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VHcm91cEJpbmRpbmciTgocVW5iaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIfCh1VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJUChxDbGFpbURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGgoGZG9tYWluGAIgASgJQgq6SAdyBRADGP0BIlsKHUNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlEjoKBmRvbWFpbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVZlcmlmaWVkRG9tYWluIk8KHVZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlwKHlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXNwb25zZRI6CgZkb21haW4YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbiJPCh1EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIgCh5EZWxldGVEYXRhc291cmNlRG9tYWluUmVzcG9uc2UingIKEFNvdXJjZURlbGVnYXRpb24SCgoCaWQYASABKAkSEQoJc291cmNlX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIOCgZtb2R1bGUYBCABKAkSDwoHYmluZGluZxgFIAEoCRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpyZXZva2VkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBJACgpyZXZvY2F0aW9uGAggASgOMiwuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhISCgpyZXZva2VkX2J5GAkgASgJIoEBChxMaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIAoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABAUgAiAEBEhcKD2luY2x1ZGVfcmV2b2tlZBgDIAEoCEIMCgpfc291cmNlX2lkIlgKHUxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlEjcKC2RlbGVnYXRpb25zGAEgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIk8KHVJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlgKHlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZRI2CgpkZWxlZ2F0aW9uGAEgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIjIKD1RyaWdnZXJJZGVudGl0eRIOCgZzb3VyY2UYASABKAkSDwoHYmluZGluZxgCIAEoCSKEAQoXRGVjbGFyZWRUcmlnZ2VyU2NoZWR1bGUSDAoEY3JvbhgBIAEoCRIQCgh0aW1lem9uZRgCIAEoCRISCgptaXNzZWRfcnVuGAMgASgJEg8KB292ZXJsYXAYBCABKAkSEAoIZGVhZGxpbmUYBSABKAMSEgoKaW5wdXRfanNvbhgGIAEoCSLnAgoaVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USEQoJcmVmZXJlbmNlGAEgASgJEhUKDWRlbGVnYXRpb25faWQYAiABKAkSDgoGbW9kdWxlGAMgASgJEg8KB2JpbmRpbmcYBCABKAkSEAoIYXVkaWVuY2UYBSABKAkSDgoGdGVuYW50GAYgASgJEhoKEm93bmVyX3ByaW5jaXBhbF9pZBgHIAEoCRIOCgZzY29wZXMYCCADKAkSDgoGYWN0aXZlGAkgASgIEi4KCmNyZWF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnJldm9rZWRfYXQYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEkAKCnJldm9jYXRpb24YDCABKA4yLC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uIr4BChNUcmlnZ2VyQ2hpbGRDb250ZXh0EhUKDWRlbGVnYXRpb25faWQYASABKAkSDgoGbW9kdWxlGAIgASgJEg8KB2JpbmRpbmcYAyABKAkSEAoIYXVkaWVuY2UYBCABKAkSDgoGc2NvcGVzGAUgAygJEg4KBmxvb2t1cBgGIAEoCBItCgltaW50ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBnJlY29yZBgIIAEoCSKgAwoYU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEg8KB3JlbGVhc2UYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEjsKCHNjaGVkdWxlGAMgASgLMikuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlZFRyaWdnZXJTY2hlZHVsZRIzCghpZGVudGl0eRgEIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlcklkZW50aXR5Eg4KBmRpZ2VzdBgFIAEoCRI2ChJsYXN0X29jY3VycmVuY2VfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjYKEm5leHRfb2NjdXJyZW5jZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTdW5kZWNsYXJhYmxlX3JlYXNvbhgIIAEoCRIaChJob3N0X3N3ZWVwX2VuYWJsZWQYCSABKAgSNQoRaG9zdF9zd2VlcF9kdWVfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIlYKHURlbGVnYXRlU291cmNlVHJpZ2dlcnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABASJjCh5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVzcG9uc2USQQoLZGVsZWdhdGlvbnMYASADKAsyLC5zYWFzLmFjY291bnRzLnYxLlRyaWdnZXJEZWxlZ2F0aW9uUmVmZXJlbmNlIlsKIkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBIuIBCiNHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXNwb25zZRI/CgtkZWNsYXJhdGlvbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEkEKC2RlbGVnYXRpb25zGAIgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5UcmlnZ2VyRGVsZWdhdGlvblJlZmVyZW5jZRI3CghjaGlsZHJlbhgDIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlckNoaWxkQ29udGV4dCJuChZSZWNvbmNpbGVTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABARIdCgllZmZlY3RfaWQYAyABKAlCCrpIB3IFEAEYgAEiTAoXUmVjb25jaWxlU291cmNlUmVzcG9uc2USEQoJZWZmZWN0X2lkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIOCgZqb2JfaWQYAyABKAkiPQocTG9va3VwUmVjb25jaWxlU291cmNlUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEYgAEi8QIKD1NvdXJjZU9wZXJhdGlvbhIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg4KBm1ldGhvZBgDIAEoCRIMCgRwYXRoGAQgASgJEg0KBXF1ZXJ5GAUgAygJEi0KDGlucHV0X3NjaGVtYRgGIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSLgoNb3V0cHV0X3NjaGVtYRgHIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSOAoGZWZmZWN0GAggASgOMiguc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VPcGVyYXRpb24uRWZmZWN0EhgKEG1heF9vdXRwdXRfYnl0ZXMYCSABKA0SDgoGZGlnZXN0GAogASgJIksKBkVmZmVjdBIWChJFRkZFQ1RfVU5TUEVDSUZJRUQQABIUChBFRkZFQ1RfUkVBRF9PTkxZEAESEwoPRUZGRUNUX01VVEFUSU9OEAIijgEKHkRlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESNQoKb3BlcmF0aW9ucxgDIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIlgKH0RlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2USNQoKb3BlcmF0aW9ucxgBIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIlQKG0xpc3RTb3VyY2VPcGVyYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQEiVQocTGlzdFNvdXJjZU9wZXJhdGlvbnNSZXNwb25zZRI1CgpvcGVyYXRpb25zGAEgAygLMiEuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VPcGVyYXRpb24iwwEKHEludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEjAKCW9wZXJhdGlvbhgDIAEoCUIdukgachgyFl5bYS16XVthLXowLTlfXXswLDYzfSQSHQoKaW5wdXRfanNvbhgEIAEoCUIJukgGcgQogIAEEhsKCWVmZmVjdF9pZBgFIAEoCUIIukgFcgMogAEifwoWU291cmNlT3BlcmF0aW9uUmVjZWlwdBIRCgllZmZlY3RfaWQYASABKAkSFAoMY29tbWl0dGVkX2F0GAIgASgJEg4KBnN0YXR1cxgDIAEoCRIXCg9wcm92aWRlcl9zdGF0dXMYBCABKA0SEwoLb3V0cHV0X2pzb24YBSABKAkibwodSW52b2tlU291cmNlT3BlcmF0aW9uUmVzcG9uc2USEwoLb3V0cHV0X2pzb24YASABKAkSOQoHcmVjZWlwdBgCIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uUmVjZWlwdCJDCiJMb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0Eh0KCWVmZmVjdF9pZBgBIAEoCUIKukgHcgUQASiAASJ5CiNQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoJZWZmZWN0X2lkGAMgASgJQgi6SAVyAyiAASJbCiRQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVzcG9uc2USEQoJZWZmZWN0X2lkGAEgASgJEhAKCHJlY2VpcHRzGAIgASgDEg4KBnN0YXR1cxgDIAEoCSJKCilMb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEogAEqtwEKEkRhdGFzb3VyY2VQcm92aWRlchIjCh9EQVRBU09VUkNFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASHgoaREFUQVNPVVJDRV9QUk9WSURFUl9HSVRIVUIQARIbChdEQVRBU09VUkNFX1BST1ZJREVSX0FQSRACEh8KG0RBVEFTT1VSQ0VfUFJPVklERVJfQ1JBV0xFUhADEh4KGkRBVEFTT1VSQ0VfUFJPVklERVJfVVBMT0FEEAQqkQEKEERhdGFzb3VyY2VTdGF0dXMSIQodREFUQVNPVVJDRV9TVEFUVVNfVU5TUEVDSUZJRUQQABIcChhEQVRBU09VUkNFX1NUQVRVU19BQ1RJVkUQARIcChhEQVRBU09VUkNFX1NUQVRVU19QQVVTRUQQAhIeChpEQVRBU09VUkNFX1NUQVRVU19ERUdSQURFRBADKrwBChZEYXRhc291cmNlTGl2ZURlbGl2ZXJ5EigKJERBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEiEKHURBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9OT05FEAESKwonREFUQVNPVVJDRV9MSVZFX0RFTElWRVJZX1NPVVJDRV9XRUJIT09LEAISKAokREFUQVNPVVJDRV9MSVZFX0RFTElWRVJZX0FQUF9XRUJIT09LEAMq1gEKEUFwaUNyZWRlbnRpYWxLaW5kEiMKH0FQSV9DUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIeChpBUElfQ1JFREVOVElBTF9LSU5EX0JFQVJFUhABEh0KGUFQSV9DUkVERU5USUFMX0tJTkRfQkFTSUMQAhIeChpBUElfQ1JFREVOVElBTF9LSU5EX0hFQURFUhADEh0KGUFQSV9DUkVERU5USUFMX0tJTkRfUVVFUlkQBBIeChpBUElfQ1JFREVOVElBTF9LSU5EX09BVVRIMhAFKqYCChxEYXRhc291cmNlQ29ubmVjdG9ySW50ZXJmYWNlEi4KKkRBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9VTlNQRUNJRklFRBAAEigKJERBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9GSUxFUxABEigKJERBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9QQUdFUxACEioKJkRBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9SRUNPUkRTEAMSKwonREFUQVNPVVJDRV9DT05ORUNUT1JfSU5URVJGQUNFX01FU1NBR0VTEAQSKQolREFUQVNPVVJDRV9DT05ORUNUT1JfSU5URVJGQUNFX0VWRU5UUxAFKuwBChhEYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSKgomREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfVU5TUEVDSUZJRUQQABIjCh9EQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9OT05FEAESJgoiREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfT1JHX0FQUBACEikKJURBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX1VTRVJfT0FVVEgQAxIsCihEQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9TVEFUSUNfU0VDUkVUEAQqlwEKFkRhdGFzb3VyY2VSZWFkZXJzTW9kZWwSKAokREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1VOU1BFQ0lGSUVEEAASKgomREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1NPVVJDRV9TQ09QRUQQARInCiNEQVRBU09VUkNFX1JFQURFUlNfTU9ERUxfVFJBTlNMQVRFRBACKu4BCg9Tb3VyY2VTeW5jUGhhc2USIQodU09VUkNFX1NZTkNfUEhBU0VfVU5TUEVDSUZJRUQQABIcChhTT1VSQ0VfU1lOQ19QSEFTRV9RVUVVRUQQARIeChpTT1VSQ0VfU1lOQ19QSEFTRV9GRVRDSElORxACEh4KGlNPVVJDRV9TWU5DX1BIQVNFX0NPTVBJTEVEEAMSIAocU09VUkNFX1NZTkNfUEhBU0VfSEFOREVEX09GRhAEEhoKFlNPVVJDRV9TWU5DX1BIQVNFX0RPTkUQBRIcChhTT1VSQ0VfU1lOQ19QSEFTRV9GQUlMRUQQBiqmAwoXU291cmNlU3luY0ZhaWx1cmVSZWFzb24SKgomU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fVU5TUEVDSUZJRUQQABIrCidTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9SQVRFX0xJTUlURUQQARIpCiVTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9DUkVERU5USUFMEAISLAooU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fQUNDRVNTX0RFTklFRBADEigKJFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX05PVF9GT1VORBAEEigKJFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX1RPT19MQVJHRRAFEi8KK1NPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0hPU1RfVU5BVkFJTEFCTEUQBhIkCiBTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9PVEhFUhAHEi4KKlNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0RFTElWRVJZX0ZBSUxFRBAIKpwBChFTb3VyY2VTeW5jVHJpZ2dlchIjCh9TT1VSQ0VfU1lOQ19UUklHR0VSX1VOU1BFQ0lGSUVEEAASHgoaU09VUkNFX1NZTkNfVFJJR0dFUl9NQU5VQUwQARIhCh1TT1VSQ0VfU1lOQ19UUklHR0VSX1NDSEVEVUxFRBACEh8KG1NPVVJDRV9TWU5DX1RSSUdHRVJfV0VCSE9PSxADKo8BChZEYXRhc291cmNlRG9tYWluU3RhdHVzEigKJERBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19VTlNQRUNJRklFRBAAEiQKIERBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19QRU5ESU5HEAESJQohREFUQVNPVVJDRV9ET01BSU5fU1RBVFVTX1ZFUklGSUVEEAIqlQMKGlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uEiwKKFNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fVU5TUEVDSUZJRUQQABIoCiRTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1JFVk9LRUQQARIpCiVTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1JFUExBQ0VEEAISLworU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9TT1VSQ0VfREVMRVRFRBADEi8KK1NPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fTUVNQkVSX1JFTU9WRUQQBBIwCixTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1BFUk1JU1NJT05fTE9TVBAFEi4KKlNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fVVNFUl9JTkFDVElWRRAGEjAKLFNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fQklORElOR19DSEFOR0VEEAcyiTgKEURhdGFzb3VyY2VTZXJ2aWNlErkDChxQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzEjUuc2Fhcy5hY2NvdW50cy52MS5QcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlIqkCwvMYbAgCEAMaEWRhdGFzb3VyY2U6aW52b2tlIhFkYXRhc291cmNlOmludm9rZSoPCglzb3VyY2VfaWQQBBgDMAE6Iwofc2Fhcy5kYXRhc291cmNlLnJlY2VpcHRzLnBydW5lZBACQANIBFADWANgAdrzGLQBCgIIPBIDCNgEGAUiAggCKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMg9zYWFzLWRhdGFzb3VyY2VKRi9zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTZXJ2aWNlL0xvb2t1cFBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNQgCBYgCBgAXIYCgZzb3VyY2USBmludm9rZRIEcmVhZBgBEuYBCiJMb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzEjsuc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlIkvC8xhHCAIQAxoPZGF0YXNvdXJjZTpyZWFkIg9kYXRhc291cmNlOnJlYWQqDwoJZWZmZWN0X2lkEAQYAzABOgIQAUABSANQA1gDYAES/AMKFUludm9rZVNvdXJjZU9wZXJhdGlvbhIuLnNhYXMuYWNjb3VudHMudjEuSW52b2tlU291cmNlT3BlcmF0aW9uUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuSW52b2tlU291cmNlT3BlcmF0aW9uUmVzcG9uc2UigQPC8xhuCAIQAxoRZGF0YXNvdXJjZTppbnZva2UiEWRhdGFzb3VyY2U6aW52b2tlKg8KCXNvdXJjZV9pZBAEGAMwATolCiFzYWFzLmRhdGFzb3VyY2Uub3BlcmF0aW9uLmludm9rZWQQBEADSARQBFgEYAHa8xivAQoCCDwSAwjYBBgFIgIIAioLVU5BVkFJTEFCTEUqElJFU09VUkNFX0VYSEFVU1RFRCoHQUJPUlRFRDIPc2Fhcy1kYXRhc291cmNlSj8vc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlU2VydmljZS9Mb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25QgIAEWICAQGABchgKBnNvdXJjZRIGaW52b2tlEgRyZWFkGAGC0+STAlU6ASoiUC92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMve29wZXJhdGlvbn06aW52b2tlEocCChtMb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb24SNC5zYWFzLmFjY291bnRzLnYxLkxvb2t1cEludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlIoABwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUANYBGABgtPkkwIvEi0vdjEvZGF0YXNvdXJjZS1vcGVyYXRpb24tcmVjZWlwdHMve2VmZmVjdF9pZH0ShQIKFExpc3RTb3VyY2VPcGVyYXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlT3BlcmF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2UijQHC8xhECAIQAxoPZGF0YXNvdXJjZTpyZWFkIg9kYXRhc291cmNlOnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAj8SPS92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMSkwIKF0RlY2xhcmVTb3VyY2VPcGVyYXRpb25zEjAuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1JlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkRlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2UikgHC8xhGCAIQBCoMCgZvcmdfaWQQAhgBMAE6Jgoic2Fhcy5kYXRhc291cmNlLm9wZXJhdGlvbi5kZWNsYXJlZBACQAFIBFADWANgAYLT5JMCQjoBKiI9L3YxL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZGF0YXNvdXJjZXMve3NvdXJjZV9pZH0vb3BlcmF0aW9ucxKsAQoPQWRkR2l0SHViU291cmNlEiguc2Fhcy5hY2NvdW50cy52MS5BZGRHaXRIdWJTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5BZGRHaXRIdWJTb3VyY2VSZXNwb25zZSJEwvMYQAgCEAQqDAoGb3JnX2lkEAIYATABOiAKHHNhYXMuZGF0YXNvdXJjZS5zb3VyY2UuYWRkZWQQAkABSARQBFgDYAESmgEKCUFkZFNvdXJjZRIiLnNhYXMuYWNjb3VudHMudjEuQWRkU291cmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuQWRkU291cmNlUmVzcG9uc2UiRMLzGEAIAhAEKgwKBm9yZ19pZBACGAEwATogChxzYWFzLmRhdGFzb3VyY2Uuc291cmNlLmFkZGVkEAJAAUgEUARYA2ABEo8BChRHZXREYXRhc291cmNlQ2F0YWxvZxItLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlQ2F0YWxvZ1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSANQAlgCYAESggEKC0xpc3RTb3VyY2VzEiQuc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VzUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEnwKCUdldFNvdXJjZRIiLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEp4BCgpTeW5jU291cmNlEiMuc2Fhcy5hY2NvdW50cy52MS5TeW5jU291cmNlUmVxdWVzdBokLnNhYXMuYWNjb3VudHMudjEuU3luY1NvdXJjZVJlc3BvbnNlIkXC8xhBCAIQBCoMCgZvcmdfaWQQAhgBMAE6IQodc2Fhcy5kYXRhc291cmNlLnNvdXJjZS5zeW5jZWQQAkABSARQBFgCYAESiAEKDUdldFNvdXJjZVN5bmMSJi5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVN5bmNSZXF1ZXN0Gicuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VTeW5jUmVzcG9uc2UiJsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEqUBCgxEZWxldGVTb3VyY2USJS5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZVNvdXJjZVJlcXVlc3QaJi5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZVNvdXJjZVJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLnNvdXJjZS5yZW1vdmVkEAJAAUgEUAJYAmABEsQBChNCZWdpbkdpdEh1YkFwcFNldHVwEiwuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkdpdEh1YkFwcFNldHVwUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlIlDC8xhMCAIQBCoMCgZvcmdfaWQQAhgBMAE6LAooc2Fhcy5kYXRhc291cmNlLmdpdGh1Yl9hcHAuc2V0dXBfc3RhcnRlZBACQAFIBFACWARgARLPAQoWQ29tcGxldGVHaXRIdWJBcHBTZXR1cBIvLnNhYXMuYWNjb3VudHMudjEuQ29tcGxldGVHaXRIdWJBcHBTZXR1cFJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZSJSwvMYTggCEAQqDAoGb3JnX2lkEAIYATABOi4KKnNhYXMuZGF0YXNvdXJjZS5naXRodWJfYXBwLnNldHVwX2NvbXBsZXRlZBACQAFIBFAEWANgARLNAQoYTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwEjEuc2Fhcy5hY2NvdW50cy52MS5NaWdyYXRlR2l0SHViU291cmNlVG9BcHBSZXF1ZXN0GjIuc2Fhcy5hY2NvdW50cy52MS5NaWdyYXRlR2l0SHViU291cmNlVG9BcHBSZXNwb25zZSJKwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5jcmVkZW50aWFsLnVwZGF0ZWQQAkABSARQAlgDYAES1QEKGkJlZ2luRGF0YXNvdXJjZUFjY291bnRMaW5rEjMuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkRhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNC5zYWFzLmFjY291bnRzLnYxLkJlZ2luRGF0YXNvdXJjZUFjY291bnRMaW5rUmVzcG9uc2UiTMLzGEgIAhADKgwKBm9yZ19pZBACGAEwATooCiRzYWFzLmRhdGFzb3VyY2UuYWNjb3VudF9saW5rX3N0YXJ0ZWQQAkABSARQAlgEYAES2AEKHUNvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rEjYuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVzcG9uc2UiRsLzGEIIAhADKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLmRhdGFzb3VyY2UuYWNjb3VudF9saW5rZWQQAkABSARQBFgDYAEStQEKHExpc3RNeURhdGFzb3VyY2VBY2NvdW50TGlua3MSNS5zYWFzLmFjY291bnRzLnYxLkxpc3RNeURhdGFzb3VyY2VBY2NvdW50TGlua3NSZXF1ZXN0GjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEtQBChtEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmsSNC5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkjC8xhECAIQAyoMCgZvcmdfaWQQAhgBMAE6JAogc2Fhcy5kYXRhc291cmNlLmFjY291bnRfdW5saW5rZWQQAkABSARQAlgDYAESowEKFkdldERhdGFzb3VyY2VEaXJlY3RvcnkSLy5zYWFzLmFjY291bnRzLnYxLkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlRGlyZWN0b3J5UmVzcG9uc2UiJsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABErcBChNCaW5kRGF0YXNvdXJjZUdyb3VwEiwuc2Fhcy5hY2NvdW50cy52MS5CaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlIkPC8xg/CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hwobc2Fhcy5kYXRhc291cmNlLmdyb3VwX2JvdW5kEAJAAUgEUAJYA2ABEr8BChVVbmJpbmREYXRhc291cmNlR3JvdXASLi5zYWFzLmFjY291bnRzLnYxLlVuYmluZERhdGFzb3VyY2VHcm91cFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLlVuYmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlIkXC8xhBCAIQBCoMCgZvcmdfaWQQAhgBMAE6IQodc2Fhcy5kYXRhc291cmNlLmdyb3VwX3VuYm91bmQQAkABSARQAlgDYAESwAEKFUNsYWltRGF0YXNvdXJjZURvbWFpbhIuLnNhYXMuYWNjb3VudHMudjEuQ2xhaW1EYXRhc291cmNlRG9tYWluUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuQ2xhaW1EYXRhc291cmNlRG9tYWluUmVzcG9uc2UiRsLzGEIIAhAEKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLmRhdGFzb3VyY2UuZG9tYWluX2NsYWltZWQQAkABSARQAlgDYAESxAEKFlZlcmlmeURhdGFzb3VyY2VEb21haW4SLy5zYWFzLmFjY291bnRzLnYxLlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5WZXJpZnlEYXRhc291cmNlRG9tYWluUmVzcG9uc2UiR8LzGEMIAhAEKgwKBm9yZ19pZBACGAEwATojCh9zYWFzLmRhdGFzb3VyY2UuZG9tYWluX3ZlcmlmaWVkEAJAAUgEUAJYA2ABEsMBChZEZWxldGVEYXRhc291cmNlRG9tYWluEi8uc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuRGVsZXRlRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmRvbWFpbl9yZW1vdmVkEAJAAUgEUAJYA2ABEqABChVMaXN0U291cmNlRGVsZWdhdGlvbnMSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VEZWxlZ2F0aW9uc1JlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARLHAQoWUmV2b2tlU291cmNlRGVsZWdhdGlvbhIvLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU291cmNlRGVsZWdhdGlvblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZSJKwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5kZWxlZ2F0aW9uLnJldm9rZWQQAkABSARQAlgDYAES6wEKFkRlbGVnYXRlU291cmNlVHJpZ2dlcnMSLy5zYWFzLmFjY291bnRzLnYxLkRlbGVnYXRlU291cmNlVHJpZ2dlcnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVzcG9uc2UibsLzGGoIAhAEKgwKBm9yZ19pZBACGAEwATpKCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5jcmVhdGVkCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5yZXZva2VkEAJAAUgEUAJYA2ABErIBChtHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb24SNC5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKDAwoPUmVjb25jaWxlU291cmNlEiguc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXNwb25zZSKaAsLzGGoIAhADGhFkYXRhc291cmNlOmludm9rZSIRZGF0YXNvdXJjZTppbnZva2UqDwoJc291cmNlX2lkEAQYAzABOiEKHXNhYXMuZGF0YXNvdXJjZS5zb3VyY2Uuc3luY2VkEAJAA0gEUAJYAmAB2vMYpwEKAgg8EgMI2AQYBSICCAIqC1VOQVZBSUxBQkxFKhJSRVNPVVJDRV9FWEhBVVNURUQqB0FCT1JURUQyD3NhYXMtZGF0YXNvdXJjZUo5L3NhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVNlcnZpY2UvTG9va3VwUmVjb25jaWxlU291cmNlUIAgWIAgYAFyGAoGc291cmNlEgZpbnZva2USBHJlYWQYARK/AQoVTG9va3VwUmVjb25jaWxlU291cmNlEi4uc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBSZWNvbmNpbGVTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXNwb25zZSJLwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUAJYAmABQrcBChRjb20uc2Fhcy5hY2NvdW50cy52MUIPRGF0YXNvdXJjZVByb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_codefly_runnable_v0_options, file_google_api_annotations, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * GitHubDatasourceConfig is the public, non-secret configuration of a GitHub @@ -2416,6 +2416,16 @@ export const RevokeSourceDelegationResponseSchema: GenMessage { test("the playground states whose authority an occurrence would run under", async ({ page, }) => { - await page.goto("/admin/platform/unattended-work"); + await page.goto("/admin/unattended-work"); await page.waitForURL( - (url) => url.pathname === "/admin/platform/unattended-work", + (url) => url.pathname === "/admin/unattended-work", { timeout: 15000 }, ); expect(page.url()).not.toContain("/auth/login"); diff --git a/module/services/frontend/code/ui-inventory.json b/module/services/frontend/code/ui-inventory.json index e9f85200b..ed18d50b9 100644 --- a/module/services/frontend/code/ui-inventory.json +++ b/module/services/frontend/code/ui-inventory.json @@ -2498,7 +2498,7 @@ "remaining": "Owner stories or a reviewed nonvisual exclusion still required." }, { - "source": "src/app/admin/platform/unattended-work/page.tsx", + "source": "src/app/admin/unattended-work/page.tsx", "imports": [ "@/components/auth/role-gate", "@/features/unattended-work" diff --git a/module/services/frontend/frontend.bindings.codefly.yaml b/module/services/frontend/frontend.bindings.codefly.yaml index e6aaf310a..ef6e5ca90 100644 --- a/module/services/frontend/frontend.bindings.codefly.yaml +++ b/module/services/frontend/frontend.bindings.codefly.yaml @@ -291,6 +291,24 @@ navigation: - plugin_registry - sidebar order: 410 + # The identity unattended work runs under, beside the sources it is bound to. + # Organization administrators own this: every RPC behind the page is org-scoped + # and ORG_ADMIN, so `access: admin` matches both the page's own gate and the + # boundary the route catalog derives from `/admin/` (issue #1121). While the + # page sat under /admin/platform/ the catalog derived `super_admin`, which + # disagreed with the gate and left org admins with no way to reach it. + - id: admin-unattended-work + plugin: core-users + label: Unattended work + path: /admin/unattended-work + icon: KeyRound + group: Integrations + access: admin + surfaces: + - command_palette + - plugin_registry + - sidebar + order: 415 - id: admin-sso plugin: core-users label: Single Sign-On diff --git a/module/services/frontend/generated/plugin-catalog.json b/module/services/frontend/generated/plugin-catalog.json index 0bbe6918e..9f9ade780 100644 --- a/module/services/frontend/generated/plugin-catalog.json +++ b/module/services/frontend/generated/plugin-catalog.json @@ -137,12 +137,6 @@ "match": "FRONTEND_ROUTE_MATCH_EXACT", "access": "FRONTEND_ROUTE_ACCESS_SUPER_ADMIN" }, - { - "path": "/admin/platform/unattended-work", - "source_path": "src/app/admin/platform/unattended-work/page.tsx", - "match": "FRONTEND_ROUTE_MATCH_EXACT", - "access": "FRONTEND_ROUTE_ACCESS_SUPER_ADMIN" - }, { "path": "/admin/platform/waitlist", "source_path": "src/app/admin/platform/waitlist/page.tsx", @@ -191,6 +185,12 @@ "match": "FRONTEND_ROUTE_MATCH_PARAMETER", "access": "FRONTEND_ROUTE_ACCESS_AUTHENTICATED" }, + { + "path": "/admin/unattended-work", + "source_path": "src/app/admin/unattended-work/page.tsx", + "match": "FRONTEND_ROUTE_MATCH_EXACT", + "access": "FRONTEND_ROUTE_ACCESS_ADMIN" + }, { "path": "/admin/users", "source_path": "src/app/admin/users/page.tsx", @@ -723,6 +723,21 @@ ], "order": 410 }, + { + "id": "admin-unattended-work", + "plugin": "core-users", + "label": "Unattended work", + "path": "/admin/unattended-work", + "icon": "KeyRound", + "group": "Integrations", + "access": "FRONTEND_ROUTE_ACCESS_ADMIN", + "surfaces": [ + "FRONTEND_NAVIGATION_SURFACE_COMMAND_PALETTE", + "FRONTEND_NAVIGATION_SURFACE_PLUGIN_REGISTRY", + "FRONTEND_NAVIGATION_SURFACE_SIDEBAR" + ], + "order": 415 + }, { "id": "admin-sso", "plugin": "core-users", From 0d0b0b4576c7016adb6b34c761a773805b4b3961 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:30:38 -0400 Subject: [PATCH 03/20] feat(accounts,frontend): name, bound and expose the module identity minted for ONE tenant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Item 13 of #1121: the gap every consumer of unattended publication is bounded by. A module acting with no person present could mint an identity for its deployment, and a consumer that admits a publication for a tenant takes the publisher's tenant from the verified capability — so a module that cannot name an organization cannot publish for one at all. THE CAPABILITY ALREADY EXISTS, AND THAT IS THE FINDING. `MintModuleOperationContext` takes `organization_id`, the gateway's `/modules/_operation-context` relays it, `moduleMintOrganization` decides which organizations may be named, and `TestSharedInstallationMintSelectsOnlyConsentingOrganization` has been minting for two organizations and comparing what each capability carries. What was missing was that nothing named it, documented it, or let an operator see it: WORK_CONTEXTS.md said the tenant was "the tenant the envelope declares", the verifier's own contract said the mint "never seals another", and a composition had nothing to read. So this adds no second mint — that would be a second mechanism for a job one seam already does. What it adds: - the rule, stated where the mint is: the organization is the one the capability acts FOR; a pinned principal names its own tenant and no other, an unpinned one declaring `cross_tenant` names one per mint, one declaring neither names nothing — and each named organization is then held to its own live installation, with the binding re-read AFTER that installation's scope narrows it. An organization consenting to fewer headless scopes gets fewer; one consenting to none is refused. The bound is per tenant, which is exactly what one identity per deployment could not be. - `PlatformAdminService/ResolveModuleTenantIdentity` (`super_admin`): what one mint would assert — principal, tenant, audience, scopes, lifetime, revision, and whether the principal is shared — or the refusal, in one of four forms because what an operator does about each differs; plus the trail of capabilities actually issued, which is the only record of one. - **it issues no capability.** The mint authenticates with the module pod's projected ServiceAccount token held against the approved build; a browser has none, and a surface that answered an operator with a module's capability would be a second spelling of that module's identity. The read resolves the decision from the same declared facts and stops before the signature. The playground's container test records every request the page makes and asserts it is the resolution alone. - the playground, `/admin/platform/module-identity` (owner's rule, handbook#291). Docs: MODULE.md, WORK_CONTEXTS.md § A module identity minted for one tenant, accounts/AGENTS.md, CLAIM_INVENTORY.md (three new rows, one of them `planned` against the client). NOT CLOSED, and named rather than worked around: the published module-authority client's `ModuleWorkContext(ctx)` and `MintModuleOperationContext(ctx, binding)` take no tenant, so a module consuming that client still mints one identity for its whole deployment and cannot reach this capability through the client it is given. That is a client-side gap in `saas-sdk-go/moduleauthority`. Co-Authored-By: Claude Opus 5 (1M context) --- CLAIM_INVENTORY.md | 8 + MODULE.md | 21 +- module/WORK_CONTEXTS.md | 62 +- .../api/accounts/authority/contract.binpb | Bin 1157172 -> 1165797 bytes .../saas/accounts/v1/platform_admin.proto | 112 +++ .../api/accounts/connect/contract.binpb | Bin 1157172 -> 1165797 bytes .../saas/accounts/v1/platform_admin.proto | 112 +++ module/contracts/api/catalog.codefly.json | 5 +- module/module.package.codefly.yaml | 4 +- module/services/accounts/AGENTS.md | 75 +- module/services/accounts/AUTHZ_MATRIX.md | 5 +- .../platform_admin_authorization_test.go | 8 + .../adapters/platform_module_envelope_rpcs.go | 62 ++ .../adapters/rest_registration_catalog_gen.go | 1 + .../code/pkg/business/introspection.go | 1 + .../pkg/business/module_operation_context.go | 18 + .../pkg/business/module_tenant_identity.go | 186 +++++ .../business/module_tenant_identity_test.go | 162 +++++ .../platform_admin.connect.go | 86 ++- .../gen/saas/accounts/v1/platform_admin.pb.go | 670 ++++++++++++++---- .../saas/accounts/v1/platform_admin.pb.gw.go | 166 +++-- .../accounts/v1/platform_admin_grpc.pb.go | 94 ++- .../generated/api-contract-surfaces.json | 1 + .../accounts/generated/authz-methods.json | 37 + .../accounts/generated/gateway-routes.json | 48 ++ .../generated/openapi-raw/api.swagger.json | 131 ++++ .../accounts/generated/rest-surface.json | 16 + .../accounts/generated/service-catalog.json | 52 ++ .../accounts/openapi/api.swagger.json | 131 ++++ .../saas/accounts/v1/platform_admin.proto | 112 +++ .../auth-gateway/code/authz_catalog_gen.go | 2 + .../auth-gateway/code/routing_catalog_gen.go | 2 + .../code/routing_rest_catalog_gen.go | 1 + .../generated/contract/catalog.codefly.json | 3 +- .../generated/contract/contract.binpb | Bin 1157172 -> 1165797 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../gen/saas/accounts/v1/platform_admin_pb.ts | 199 +++++- .../code/packages/saas-sdk/package.json | 2 +- .../admin/platform/module-identity/page.tsx | 34 + .../code/src/app/admin/platform/page.tsx | 7 + .../features/module-tenant-identity/index.ts | 2 + .../module-tenant-identity/service/queries.ts | 29 + ...ant-identity-playground.container.test.tsx | 156 ++++ .../ui/module-tenant-identity-playground.tsx | 316 +++++++++ .../gen/saas/accounts/v1/platform_admin_pb.ts | 199 +++++- .../gen/saas/frontend/v1/plugin_catalog.ts | 1 + .../services/frontend/code/ui-inventory.json | 31 + .../frontend/generated/plugin-catalog.json | 6 + 48 files changed, 3109 insertions(+), 269 deletions(-) create mode 100644 module/services/accounts/code/pkg/business/module_tenant_identity.go create mode 100644 module/services/accounts/code/pkg/business/module_tenant_identity_test.go create mode 100644 module/services/frontend/code/src/app/admin/platform/module-identity/page.tsx create mode 100644 module/services/frontend/code/src/features/module-tenant-identity/index.ts create mode 100644 module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts create mode 100644 module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx create mode 100644 module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx diff --git a/CLAIM_INVENTORY.md b/CLAIM_INVENTORY.md index 3f8c29b20..7f3c4b1dd 100644 --- a/CLAIM_INVENTORY.md +++ b/CLAIM_INVENTORY.md @@ -98,6 +98,11 @@ must not be inferred from a row here. See | A reconcile capability is bound to the source whose DELEGATION it carries, not just to a scoped one | WORK_CONTEXTS.md § Triggers | `TestAChildFromOneSourcesDelegationIsRefusedOnAnother` (`pkg/adapters`) mints a real capability whose binding scopes two sources, presents it through `authenticateSourceOperationContext`, and asserts the delegation's own source is admitted and the other refused; red under both mutations (dropping the gate, and re-discarding the delegation) | implemented | accounts | | One occurrence's enqueue key is scoped by organization and source | accounts/AGENTS.md § A datasource source is refreshed by three things | `TestReconcileOccurrenceKeyIsScopedByOrgAndSource` (`pkg/business`): two organizations' sources under one caller-chosen effect identity produce different keys, each key names all three parts, and two attempts of one occurrence still agree | implemented | accounts | | `DelegateSourceTriggers` is additive and refuses another member's personal source | WORK_CONTEXTS.md § Triggers | `TestDelegateSourceTriggersLeavesAnotherModulesDelegationAlone`, `TestDelegateSourceTriggersRefusesAnotherMembersPersonalSource` (`pkg/business`) assert the delegation rows: another module's survives active and owner-intact, nothing is revoked, and the owner's own call records | implemented | accounts | +| The backfill records a reference only for a module that EXECUTES declared work, and replaces the executor's unusable one | accounts/AGENTS.md § Unattended work | `TestDelegateSourceTriggersLeavesAnotherModulesDelegationAlone` asserts a consuming module gets nothing; `TestDelegateSourceTriggersReplacesTheExecutorsStaleDelegation` asserts a stale-digest delegation is revoked as `binding_changed` and replaced while a current one is left untouched. Both red under the matching mutation (dropping the executor filter; always skipping an existing row) | implemented | accounts | +| A declared trigger's reference arm names only a delegation its executor can mint from | accounts/AGENTS.md § Unattended work; WORK_CONTEXTS.md § Triggers | `TestTriggerDeclarationNamesOnlyADelegationItsExecutorCanUse` (`pkg/business`) removes each of the three facts on its own — a delegation addressed to another module, a binding no longer declared, an authority that did not resolve — and asserts the declaration names no identity, plus that an unusable row never shadows a usable one | implemented | accounts | +| A refused reconcile pass enqueues nothing | accounts/AGENTS.md § A datasource source is refreshed by three things | `TestReconcileSourceOperation_RefusedPassEnqueuesNothing` (`pkg/business`): an effect identity re-presented for another source, and one re-presented with another request digest, are both refused with no job for them anywhere; red when the pre-check is removed | implemented | accounts | +| The stopgap sweep can be switched off, and the reader is told which schedule fires and when | accounts/AGENTS.md § Unattended work | `TestTheStopgapSweepCanBeSwitchedOff` (`pkg/business`) asserts the default is ON, that a disabled sweep enqueues nothing and bumps nothing, and that the declaration's `host_sweep_due_at` is the STORED instant and differs from the cron's; `source-trigger-schedules.container.test.tsx` asserts both rows render and that the host's row says "off" with no instant | implemented | accounts, frontend | +| `source.synced` says whether anybody was present for the pass | accounts/AGENTS.md § Unattended work | the adapter marks a delegated pass (`business.WithUnattendedPass`) and the observation records `unattended` and the delegation id, declared optional in payload v2. The actor stays the delegating person, which ADR 0008 puts in the trail deliberately | implemented | accounts | | The declaration carries the field names the executing module validates | MODULE.md § Unattended work; WORK_CONTEXTS.md § Triggers | `TestTriggerDeclarationMatchesTheContractFieldNames`, `TestTriggerDeclarationUsesDeclaredPolicyValues` (`pkg/business`) | implemented | accounts | | Only intervals a five-field cron can name are declarable; the rest are refused by name | accounts/AGENTS.md § A datasource source is refreshed by three things | `TestTriggerScheduleSpellsOnlyTheIntervalsCronCanName` (`pkg/business`) | implemented | accounts | | An occurrence under the reference arm is minted from current facts, and the next one after a revocation is refused | MODULE.md § Unattended work; WORK_CONTEXTS.md § Triggers | `TestTriggerOccurrenceMintsFromTheReferenceAndIsRefusedAfterRevocation` (`pkg/adapters`) | implemented | accounts | @@ -109,6 +114,9 @@ must not be inferred from a row here. See | No surface in this repository reaches a source sync without an effect identity | MODULE.md § Unattended work; saas-ui README § Datasources | `reconcile-is-the-only-manual-pass.test.ts` walks the TypeScript AST of `src` and the published packages and fails on any `syncSource` on a datasource client | implemented | frontend | | A person's pass is listed beside the declared schedule's occurrences, with its effect identity | MODULE.md § Unattended work | `source-trigger-schedules.container.test.tsx` asserts the manual row is in the SAME `admissions` table as the two scheduled rows, and that a recovered receipt replaces its row rather than adding one | implemented | frontend | | Every pass a source ever took, listed | — | none, and none is possible here: a receipt is recovered by identity and never enumerated, so the panel shows only the passes this browser admitted and says so | not claimed | accounts | +| A module identity minted for ONE tenant, bounded by that tenant's own installation | accounts/AGENTS.md § A module identity minted for one tenant; WORK_CONTEXTS.md § A module identity minted for one tenant | `TestSharedInstallationMintSelectsOnlyConsentingOrganization` (`pkg/business`) mints for two organizations and compares what each capability carries; `TestResolveModuleTenantIdentityAnswersWhatOneTenantsMintWouldAssert` and `TestResolveModuleTenantIdentityRefusesATenantThePinForbids` hold the operator read to the same four answers the mint gives | implemented | accounts | +| An operator can read what a module's per-tenant capability would carry, without one being issued | accounts/AGENTS.md § A module identity minted for one tenant | `module-tenant-identity-playground.container.test.tsx` records every request the page makes and asserts it is the resolution alone; the authorization table pins `ResolveModuleTenantIdentity` at `super_admin` | implemented | accounts, frontend | +| A module reaching the per-tenant mint through the client it is given | WORK_CONTEXTS.md § A module identity minted for one tenant | none: `saas-sdk-go/moduleauthority`'s `ModuleWorkContext(ctx)` and `MintModuleOperationContext(ctx, binding)` take no tenant, so a module consuming that client mints one identity for the whole deployment. The host accepts the organization on both mints and the gateway relays it | planned | the module-authority client | | A DECLARED schedule actually coming due, in a composed environment | MODULE.md § Unattended work ("what is not proven") | none — nothing in this repository composes a module that executes declared work, and the module that fires triggers keys one installed trigger per release name while this host declares every source under one | planned | #1081 | ## Handbook and interface surfaces diff --git a/MODULE.md b/MODULE.md index 76bb62366..000dbc134 100644 --- a/MODULE.md +++ b/MODULE.md @@ -251,7 +251,23 @@ arm is planned and has never been emitted. The depth is in [services/accounts/AGENTS.md](./module/services/accounts/AGENTS.md#unattended-work-whose-authority-an-occurrence-runs-under) and [WORK_CONTEXTS.md](./module/WORK_CONTEXTS.md#triggers-for-work-no-person-started); -the playground is `/admin/platform/unattended-work`. +the playground is `/admin/unattended-work`. + +**A module identity minted for one tenant.** Beside the trigger identities +above, a module acting with no person present can present itself as acting +*for* a tenant rather than as itself: the headless operation mint names the +organization, and the capability's tenant is that organization, its scopes that +organization's installation narrowed to the binding's `headless_scopes`. Which +organizations it may name is the envelope's (a pinned principal names its own +tenant and no other; an unpinned one declaring `cross_tenant` names one per +mint), and each is then held to its own consent — so the bound is per tenant, +not per deployment. That is what a consumer admitting a publication for a tenant +reads the publisher's tenant from. +`PlatformAdminService/ResolveModuleTenantIdentity` and +`/admin/platform/module-identity` answer what one mint would assert, or why it +would be refused, and issue nothing; the client-side gap that still stops a +module reaching it (`saas-sdk-go/moduleauthority` takes no tenant) is named in +[WORK_CONTEXTS.md](./module/WORK_CONTEXTS.md#a-module-identity-minted-for-one-tenant). ## The host as an OAuth 2.1 authorization server @@ -655,7 +671,8 @@ Every page lives under `frontend/code/src/app/admin/`: | `/admin/billing` | `OpenBillingPortal`, `ListInvoices` | | `/admin/platform/admins` | platform role grant/revoke | | `/admin/platform/organizations` | `ListAllOrganizations`, `GetOrganizationRoster`, `CreateOrganization` (with `owner_user_id`), `UpdateOrganization`, `AddMember`, `RemoveMember`, `DeleteOrganization` | -| `/admin/platform/unattended-work` | `ListSources`, `GetSourceTriggerDeclaration`, `DelegateSourceTriggers`, `RevokeSourceDelegation`, `ReconcileSource` — the playground for whose authority work no person started runs under | +| `/admin/unattended-work` | `ListSources`, `GetSourceTriggerDeclaration`, `DelegateSourceTriggers`, `RevokeSourceDelegation`, `ReconcileSource` — the playground for whose authority work no person started runs under | +| `/admin/platform/module-identity` | `ResolveModuleTenantIdentity` — what a module's capability for ONE tenant would carry, or why it would be refused. It issues no capability | Client-side gating uses `` (`src/components/auth/role-gate.tsx`) — display-only; backend remains authoritative. diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index 0fee734e3..63239303a 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -363,7 +363,8 @@ The host mints a child Work Context with: - owner = the module's own service principal (kind `service`), acting directly: no actor hop, as core models an owner acting, with the seal naming its installation and the build its pod runs; -- tenant = the tenant the module envelope's grant declares for that principal; +- tenant = **the organization the capability acts FOR**, which the caller names + and the envelope decides (see below); - a 60-second lifetime and the idempotent replay policy; - an authorization revision that is a digest of the module's whole derived registry entry (`business.ModuleOperationContextRevision`, never zero). @@ -413,6 +414,63 @@ A composed module reaches it through the gateway's `POST /modules/_operation-context` ([services/auth-gateway/AGENTS.md](./services/auth-gateway/AGENTS.md)), which brokers to accounts exactly as `/modules/_work-context` does. +### A module identity minted for ONE tenant + +This mint **is** the per-tenant module identity, and it is worth naming rather +than leaving as a parameter nobody documents (issue #1121). `organization_id` on +the request is the organization the capability acts for: its tenant is that +organization, so a module can present itself as *acting for a tenant* instead of +as itself. That is what a consumer admitting a publication for a tenant needs +from a publisher with nobody signed in — it takes the tenant from the verified +capability, where this host has already decided the principal may speak for it, +never from a request field it would have to trust. + +**Which organizations may be named is the envelope's, never the caller's.** +`moduleMintOrganization` decides it, identically for the identity mint and this +one: + +| The envelope declares | It may mint for | +| --- | --- | +| `tenant: ` | that organization only; another is `PermissionDenied`. `cross_tenant` beside a pin changes nothing | +| no tenant, `cross_tenant: true` | one organization per mint, named on the request | +| no tenant, no `cross_tenant` | nothing: naming an organization is not authority over it | + +**And every named organization is then held to its own consent.** The mint +re-reads the binding *after* `authorizeModuleCapabilityFromSnapshot` intersects +that organization's installation scope, so what a per-tenant capability carries +is that tenant's consent and not the deployment's: an organization that +consented to fewer headless scopes gets fewer, and one that consented to none +leaves the binding with no headless scopes and is refused. The bound is +therefore per tenant, which a single deployment-wide identity could not be. +Proved by `TestSharedInstallationMintSelectsOnlyConsentingOrganization`, which +mints for two organizations and compares what each capability carries. + +**Reading it back, and the playground.** +`PlatformAdminService/ResolveModuleTenantIdentity` answers what one mint would +assert for a module, a binding and an organization — the principal, the tenant, +the audience, the scopes, the lifetime, the revision, and whether the principal +is shared — or the refusal, in one of four forms, because what an operator does +about each is different: declare the principal, widen the envelope's tenancy, +install the module there, or consent to the binding's headless scopes there. +Beside it it answers the trail of capabilities actually issued, which is the +only record of one. `/admin/platform/module-identity` drives it. + +**It issues no capability, and that is the shape rather than a limitation.** The +mint authenticates with the module pod's projected ServiceAccount token held +against the build its authority document approves; a browser has none, and a +surface that answered an operator with a module's capability would be a second +spelling of that module's identity. So the read resolves the decision from the +same declared facts and stops before the signature. + +**What is still owed elsewhere.** The host accepts the organization on both +mints and the gateway relays it, but the published module-authority client's +`ModuleWorkContext(ctx)` and `MintModuleOperationContext(ctx, binding)` take no +tenant — so a module consuming that client mints one identity for the whole +deployment and cannot name an organization at all. That is a client-side gap in +`saas-sdk-go/moduleauthority`, not here, and until it closes a module cannot +reach this capability through the client it is given. Nothing in this repository +works around it. + ## Operation contexts from a source delegation A datasource source's sync is neither of the cases above. It runs as a module @@ -730,7 +788,7 @@ against them. The children are read from `saas.datasource.delegation.used`, because that trail is the only record of one — a capability lives about a minute and nothing caches it — so a revoked reference shows up as the next occurrence having no child at all. The playground that drives the whole walk is -`/admin/platform/unattended-work`. +`/admin/unattended-work`. **What the reconcile trigger's occurrences admit, and the scope that binds them.** `DatasourceService/ReconcileSource` and `LookupReconcileSource` are diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 791a05d33337a12a7d82bf2cde1a48ae200f9c73..7e055997b4caf7907b5efa1ab25f1867a1f86ad1 100644 GIT binary patch delta 34984 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 delta 27337 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 A$^ZZW diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto index 4e955b079..271f1452b 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto @@ -576,6 +576,100 @@ message PutModuleEnvelopeResponse { ModuleEnvelope envelope = 1; } +// --------------------------------------------------------------------------- +// A module identity minted for ONE tenant. +// +// A composed module acting with no person present mints a Work Context for one +// of its installed operation bindings (MintModuleOperationContext) and names +// the organization it is acting FOR. That is what lets a module present itself +// as acting for a tenant rather than as itself: the capability's tenant is the +// organization named, its scopes are that organization's installation narrowed +// to the binding's headless_scopes, it lives a minute, and every issuance is +// recorded. +// +// Which organizations it may name is not the caller's to choose. A principal +// the envelope PINS to a tenant may name that tenant and no other; an unpinned +// principal declaring cross_tenant may name an organization that has a live +// installation consenting to the binding, and nothing else. An organization +// whose installation consents to less gets less, and one that consents to +// nothing is refused — so the bound is per tenant, not per deployment. +// +// This read answers what that mint would assert, for an operator, WITHOUT +// issuing a capability. A browser holds no module identity (the mint requires +// the pod's projected ServiceAccount token, held against the build its +// authority document approves), and a path that handed an operator a module's +// capability would be a second spelling of that identity — so there is +// deliberately no such path, here or anywhere. +// --------------------------------------------------------------------------- + +message ResolveModuleTenantIdentityRequest { + // prefix is the module principal's registration prefix and binding the key + // of its operation_audiences entry. + string prefix = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 63 + pattern: "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" + }]; + string binding = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 128 + }]; + // organization_id is the tenant the capability would act for. + string organization_id = 3 [(buf.validate.field).string.uuid = true]; +} + +// ModuleTenantIdentity is what one per-tenant mint would assert. Every field is +// the host's: nothing here is requestable. +message ModuleTenantIdentity { + // principal_id is the module's own service principal, which owns the + // capability and acts directly — there is no person and no actor hop. + string principal_id = 1; + // tenant is the organization it would act for, and audience the service the + // binding declares it may call. A binding whose audience is the module's own + // prefix is one the module admits itself, which is how unattended work is + // admitted by the module that executes it. + string tenant = 2; + string audience = 3; + // scopes is exactly what the capability would carry, as + // `kind:action[:resource]`: the binding's headless_scopes narrowed by THIS + // organization's installation scope. Never its invoke_scopes, which describe + // authority exercised on a person's behalf. + repeated string scopes = 4; + // lifetime_seconds is the fixed lifetime every such capability carries. + int64 lifetime_seconds = 5; + // revision is the authorization revision it would be sealed with: a digest + // of the module's whole effective entry, so any change to the entry stops + // confirming every capability minted under the old one. + uint64 revision = 6; + // shared says the principal is unpinned and declares cross_tenant, so it may + // act for more than one organization. A pinned principal reads false and can + // only ever name the tenant its envelope declares. + bool shared = 7; +} + +// ModuleTenantIdentityMint is one capability actually issued, as the audit +// trail recorded it. There is no other record: a capability lives a minute and +// nothing caches one. +message ModuleTenantIdentityMint { + string record = 1; + string tenant = 2; + string binding = 3; + string audience = 4; + repeated string scopes = 5; + google.protobuf.Timestamp minted_at = 6; +} + +message ResolveModuleTenantIdentityResponse { + // identity is set when a mint for this organization would be issued; refusal + // says why it would not. Exactly one is present, and a refusal is a decision + // about declared authority rather than an outage. + ModuleTenantIdentity identity = 1; + string refusal = 2; + // recent is the trail of capabilities this module was actually issued for + // this organization, newest first. + repeated ModuleTenantIdentityMint recent = 3; +} + // PlatformAdminService — cross-tenant operations for platform operators. // All RPCs require a platform_role in the JWT claims. service PlatformAdminService { @@ -1041,4 +1135,22 @@ service PlatformAdminService { body: "*" }; } + + // ResolveModuleTenantIdentity answers what a per-tenant module operation + // context would assert for one module, one of its bindings and one + // organization — or why it would be refused. It issues no capability. + rpc ResolveModuleTenantIdentity(ResolveModuleTenantIdentityRequest) returns (ResolveModuleTenantIdentityResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_SENSITIVE + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN + }; + option (google.api.http) = {get: "/v1/platform/module-tenant-identity"}; + } } diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 791a05d33337a12a7d82bf2cde1a48ae200f9c73..7e055997b4caf7907b5efa1ab25f1867a1f86ad1 100644 GIT binary patch delta 34984 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 delta 27337 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 A$^ZZW diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto index 4e955b079..271f1452b 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto @@ -576,6 +576,100 @@ message PutModuleEnvelopeResponse { ModuleEnvelope envelope = 1; } +// --------------------------------------------------------------------------- +// A module identity minted for ONE tenant. +// +// A composed module acting with no person present mints a Work Context for one +// of its installed operation bindings (MintModuleOperationContext) and names +// the organization it is acting FOR. That is what lets a module present itself +// as acting for a tenant rather than as itself: the capability's tenant is the +// organization named, its scopes are that organization's installation narrowed +// to the binding's headless_scopes, it lives a minute, and every issuance is +// recorded. +// +// Which organizations it may name is not the caller's to choose. A principal +// the envelope PINS to a tenant may name that tenant and no other; an unpinned +// principal declaring cross_tenant may name an organization that has a live +// installation consenting to the binding, and nothing else. An organization +// whose installation consents to less gets less, and one that consents to +// nothing is refused — so the bound is per tenant, not per deployment. +// +// This read answers what that mint would assert, for an operator, WITHOUT +// issuing a capability. A browser holds no module identity (the mint requires +// the pod's projected ServiceAccount token, held against the build its +// authority document approves), and a path that handed an operator a module's +// capability would be a second spelling of that identity — so there is +// deliberately no such path, here or anywhere. +// --------------------------------------------------------------------------- + +message ResolveModuleTenantIdentityRequest { + // prefix is the module principal's registration prefix and binding the key + // of its operation_audiences entry. + string prefix = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 63 + pattern: "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" + }]; + string binding = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 128 + }]; + // organization_id is the tenant the capability would act for. + string organization_id = 3 [(buf.validate.field).string.uuid = true]; +} + +// ModuleTenantIdentity is what one per-tenant mint would assert. Every field is +// the host's: nothing here is requestable. +message ModuleTenantIdentity { + // principal_id is the module's own service principal, which owns the + // capability and acts directly — there is no person and no actor hop. + string principal_id = 1; + // tenant is the organization it would act for, and audience the service the + // binding declares it may call. A binding whose audience is the module's own + // prefix is one the module admits itself, which is how unattended work is + // admitted by the module that executes it. + string tenant = 2; + string audience = 3; + // scopes is exactly what the capability would carry, as + // `kind:action[:resource]`: the binding's headless_scopes narrowed by THIS + // organization's installation scope. Never its invoke_scopes, which describe + // authority exercised on a person's behalf. + repeated string scopes = 4; + // lifetime_seconds is the fixed lifetime every such capability carries. + int64 lifetime_seconds = 5; + // revision is the authorization revision it would be sealed with: a digest + // of the module's whole effective entry, so any change to the entry stops + // confirming every capability minted under the old one. + uint64 revision = 6; + // shared says the principal is unpinned and declares cross_tenant, so it may + // act for more than one organization. A pinned principal reads false and can + // only ever name the tenant its envelope declares. + bool shared = 7; +} + +// ModuleTenantIdentityMint is one capability actually issued, as the audit +// trail recorded it. There is no other record: a capability lives a minute and +// nothing caches one. +message ModuleTenantIdentityMint { + string record = 1; + string tenant = 2; + string binding = 3; + string audience = 4; + repeated string scopes = 5; + google.protobuf.Timestamp minted_at = 6; +} + +message ResolveModuleTenantIdentityResponse { + // identity is set when a mint for this organization would be issued; refusal + // says why it would not. Exactly one is present, and a refusal is a decision + // about declared authority rather than an outage. + ModuleTenantIdentity identity = 1; + string refusal = 2; + // recent is the trail of capabilities this module was actually issued for + // this organization, newest first. + repeated ModuleTenantIdentityMint recent = 3; +} + // PlatformAdminService — cross-tenant operations for platform operators. // All RPCs require a platform_role in the JWT claims. service PlatformAdminService { @@ -1041,4 +1135,22 @@ service PlatformAdminService { body: "*" }; } + + // ResolveModuleTenantIdentity answers what a per-tenant module operation + // context would assert for one module, one of its bindings and one + // organization — or why it would be refused. It issues no capability. + rpc ResolveModuleTenantIdentity(ResolveModuleTenantIdentityRequest) returns (ResolveModuleTenantIdentityResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_SENSITIVE + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN + }; + option (google.api.http) = {get: "/v1/platform/module-tenant-identity"}; + } } diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index c9e4caa7c..8d0ad84b4 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:0a02fc7e630eb13971649710dd7b3058ab843ee281065b1532b17821c07dac8a", + "digest": "sha256:8ac64d948bd881562508736eeb88fd7031b1b862ed43bfaa58c80090d8afae29", "services": [ { "name": "ModuleCapabilitiesService", @@ -64,7 +64,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:0a02fc7e630eb13971649710dd7b3058ab843ee281065b1532b17821c07dac8a", + "digest": "sha256:8ac64d948bd881562508736eeb88fd7031b1b862ed43bfaa58c80090d8afae29", "services": [ { "name": "APIKeyService", @@ -407,6 +407,7 @@ "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope", "/saas.accounts.v1.PlatformAdminService/ReplayJob", + "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", "/saas.accounts.v1.PlatformAdminService/RevokeSession", "/saas.accounts.v1.PlatformAdminService/SearchUsers", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index ab9f8e9d2..dac1fd593 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:0a02fc7e630eb13971649710dd7b3058ab843ee281065b1532b17821c07dac8a + digest: sha256:8ac64d948bd881562508736eeb88fd7031b1b862ed43bfaa58c80090d8afae29 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:0a02fc7e630eb13971649710dd7b3058ab843ee281065b1532b17821c07dac8a + digest: sha256:8ac64d948bd881562508736eeb88fd7031b1b862ed43bfaa58c80090d8afae29 - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index 084a6f5ff..f7baac3d0 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -867,6 +867,15 @@ the values and records the other column beside them). occurrence's run outlive the capability that admitted it, and makes a revocation land on the NEXT occurrence rather than behind something held from the one before. + + `DeclareSourceTrigger` sets it **only for a delegation the executing module + can use** (#1121): a mint by source id resolves the delegation of the CALLING + module and the capability is addressed to the binding's audience, which the + executing module must itself admit — so the one usable shape is a delegation + whose binding names its own module as the audience + (`TriggerDelegationReference.usableByExecutor`). It took the first active + reference from any module before, so a deployment whose executor held none + read as "bound" while every occurrence was refused at the mint. - **`binding` is the installation-principal arm, and PLANNED.** It would name one of the release's bindings and mint from that binding's `headless_scopes` under the module's own principal (`MintModuleOperationContext`) — an ordinary @@ -931,8 +940,21 @@ the values and records the other column beside them). destructive for a backfill: it took a personal source's delegation from its owner and moved every unrelated module's authority onto the administrator, failing in-flight children's re-checks. The - backfill leaves a target that already holds an active delegation untouched and - records only for a target with none, and it refuses a personal source the + backfill records **only for a module that executes declared work** — + `SourceDelegationTarget.ExecutesDeclaredWork`, the self-admitting binding + below — because that is what the reference it answers with is for (#1121). It + recorded one for every module accepting delegations, handing the calling + administrator's authority over that source to modules whose delegation no + trigger of this host could name. A connect still delegates to every target: + that is a person establishing the source, and a consuming module's sync is the + delegation's main purpose. + + It leaves a target's LIVE delegation untouched and records for a target with + none — or with one the module's current declaration no longer supports. A + delegation whose binding digest has moved is refused by the very next mint as + `binding_changed`, so skipping it reported "nothing was missing" over a + trigger that could never mint; it is revoked here under that same reason and + replaced in one transaction. It also refuses a personal source the caller does not own (`ErrTriggerSourcePersonal` → `PermissionDenied`, told apart from `NotFound` because the source exists and is readable) — the division `ReconcileDatasourceSource` makes on the pass itself, so a reference @@ -945,7 +967,54 @@ the values and records the other column beside them). minted against them. The children come from `saas.datasource.delegation.used`, which is the only record of one: a capability lives about a minute and nothing caches it, so a revoked reference shows up as the next occurrence having no - child at all. The playground is `/admin/platform/unattended-work`. + child at all. The playground is `/admin/unattended-work`, under `/admin/` + rather than `/admin/platform/` because the route catalog derives a page's + access boundary from its path and every RPC behind this one is org-scoped and + `ORG_ADMIN` (#1121): while it sat under the platform prefix the catalog + derived `super_admin` and the page's own gate said `admin`. +- **The reader is also told what THIS HOST does about the schedule, and when** + (#1121). The declaration carries `host_sweep_enabled` and `host_sweep_due_at` + beside `next_occurrence_at`, because the declared cron and the stopgap sweep + share a period and NOT a phase: the cron comes due on the quarter or half + hour, the sweep fires on the source's stored instant, which every pass moves + to `now + interval`. Neither is aligned to the other on purpose — a cron + aligned to the stored instant would stop being derived from the interval + alone, and a stored instant aligned to the cron would make every source of one + provider come due in the same second. `DATASOURCE_RECONCILE_SWEEP_ENABLED` + (application group, ON unless `false`) is what an operator turns off when a + composed module starts consuming the declaration; it is read in two places + that are one fact — the tick is not installed, and `RunDatasourceReconcile` + itself refuses. + +## A module identity minted for one tenant + +`MintModuleOperationContext` takes the organization the capability acts FOR, and +that makes it the per-tenant module identity: a module with nobody signed in can +present itself as acting for a tenant rather than as itself, which is what a +consumer admitting a publication for a tenant reads the publisher's tenant from. + +Which organizations may be named is the envelope's and never the caller's +(`moduleMintOrganization`): a principal pinned to a tenant names that tenant and +no other, an unpinned principal declaring `cross_tenant` names one per mint, and +one declaring neither names nothing. Each named organization is then held to its +own live installation, and the binding is re-read AFTER that installation's scope +narrows it — so what a per-tenant capability carries is that tenant's consent, +not the deployment's, and an organization consenting to no headless scopes is +refused. `TestSharedInstallationMintSelectsOnlyConsentingOrganization` mints for +two organizations and compares what each carries. + +`PlatformAdminService/ResolveModuleTenantIdentity` (`super_admin`) answers what +one mint would assert, or why it would be refused, plus the trail of +capabilities actually issued (`saas.module.operation_context_minted`, the only +record of one). **It issues no capability**: the mint authenticates with the +module pod's projected ServiceAccount token held against the approved build, a +browser has none, and a surface that answered an operator with a module's +capability would be a second spelling of that module's identity. The playground +is `/admin/platform/module-identity`. + +The client-side gap that still stops a module reaching this — +`saas-sdk-go/moduleauthority` takes no tenant on either mint — is named in +[WORK_CONTEXTS.md](../WORK_CONTEXTS.md#a-module-identity-minted-for-one-tenant). ## Subject visibility is a projection, not a module's own vocabulary diff --git a/module/services/accounts/AUTHZ_MATRIX.md b/module/services/accounts/AUTHZ_MATRIX.md index b4b61945a..1ace42035 100644 --- a/module/services/accounts/AUTHZ_MATRIX.md +++ b/module/services/accounts/AUTHZ_MATRIX.md @@ -2,7 +2,7 @@ > Generated from protobuf service descriptors and `saas.policy.v1.method_policy`; only the prose description is joined from `pkg/business/introspection.go`. Do not edit by hand. Run `go generate ./pkg/business` from `module/services/accounts/code`. -Inventory: **267 RPCs** across **36 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. +Inventory: **268 RPCs** across **36 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. The compact tier is retained for compatibility. Guards, resource bindings, audit events, limiter class, and data sensitivity are descriptor-authoritative. Domain handlers may enforce stronger state-dependent rules but may not weaken this floor. @@ -207,6 +207,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit | `/saas.accounts.v1.PlatformAdminService/OverrideEntitlement` | unary | `POST /v1/platform/organizations/{org_id}/entitlements` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=ANY; impersonation=FORBIDDEN | — | — | SUCCESS: saas.entitlement.override | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Override one entitlement limit for a single organization. | | `/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope` | unary | `PUT /v1/platform/module-envelope` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN; mfa=RECENT_STEP_UP; impersonation=FORBIDDEN | — | — | SUCCESS: saas.platform.module_envelope_written | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Write the next revision of the module envelope; it takes effect at the next check. | | `/saas.accounts.v1.PlatformAdminService/ReplayJob` | unary | `POST /v1/platform/jobs/{source_job_id}:replay` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN; mfa=RECENT_STEP_UP | — | — | SUCCESS: saas.job.replayed | REQUIRED / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Idempotently copy dead-lettered work for another attempt. | +| `/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity` | unary | `GET /v1/platform/module-tenant-identity` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN | — | — | — | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Read what a per-tenant module operation context would assert for one module, binding and organization, or why it would be refused. | | `/saas.accounts.v1.PlatformAdminService/RevokePlatformRole` | unary | `DELETE /v1/platform/admins/{user_id}` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN; mfa=IF_ENROLLED_RECENT_STEP_UP; impersonation=FORBIDDEN | — | — | SUCCESS: saas.platform.role_revoked | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Revoke a platform role. | | `/saas.accounts.v1.PlatformAdminService/RevokeSession` | unary | `DELETE /v1/platform/sessions/{session_id}` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPPORT | — | — | SUCCESS: saas.session.revoked | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Revoke a single session. | | `/saas.accounts.v1.PlatformAdminService/SearchUsers` | unary | `GET /v1/platform/users` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPPORT | — | — | — | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Search across all users. | @@ -283,5 +284,5 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit - `mfa`: 5 - `org_admin`: 64 - `org_member`: 56 -- `platform_admin`: 29 +- `platform_admin`: 30 - `public`: 18 diff --git a/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go b/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go index c80996bfd..83b7cad67 100644 --- a/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go +++ b/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go @@ -101,6 +101,14 @@ func platformAdminHandlers() []platformAdminHandler { _, err := srv.PutModuleEnvelope(ctx, &gen.PutModuleEnvelopeRequest{Document: `{"revision":1}`}) return err }}, + // It reads what a module's per-tenant capability would carry in any + // organization, which is cross-tenant authority however read-only it is. + {"ResolveModuleTenantIdentity", "super_admin", func(ctx context.Context, srv *PlatformAdminServer) error { + _, err := srv.ResolveModuleTenantIdentity(ctx, &gen.ResolveModuleTenantIdentityRequest{ + Prefix: "example", Binding: "operate", OrganizationId: platformTargetD, + }) + return err + }}, {"ListFeatureFlags", "super_admin", func(ctx context.Context, srv *PlatformAdminServer) error { _, err := srv.ListFeatureFlags(ctx, &gen.ListFeatureFlagsRequest{}) return err diff --git a/module/services/accounts/code/pkg/adapters/platform_module_envelope_rpcs.go b/module/services/accounts/code/pkg/adapters/platform_module_envelope_rpcs.go index b77e492da..763b11f23 100644 --- a/module/services/accounts/code/pkg/adapters/platform_module_envelope_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/platform_module_envelope_rpcs.go @@ -3,6 +3,7 @@ package adapters import ( "context" "errors" + "time" "connectrpc.com/connect" "google.golang.org/grpc/codes" @@ -102,3 +103,64 @@ func (h *platformAdminConnectHandler) GetModuleEnvelope(ctx context.Context, req func (h *platformAdminConnectHandler) PutModuleEnvelope(ctx context.Context, req *connect.Request[gen.PutModuleEnvelopeRequest]) (*connect.Response[gen.PutModuleEnvelopeResponse], error) { return unary(ctx, req, h.inner.PutModuleEnvelope) } + +// ResolveModuleTenantIdentity answers what a per-tenant module operation +// context would assert for one module, one of its bindings and one +// organization — or why it would be refused — for a platform super_admin. +// +// IT ISSUES NO CAPABILITY, and that is the shape rather than a limitation +// (issue #1121). The mint it describes requires the module pod's projected +// ServiceAccount token, held against the build its authority document approves; +// an operator's browser has none, and a path that answered one with a module's +// capability would be a second spelling of that module's identity. So this +// reads the decision and stops before the signature. +func (s *PlatformAdminServer) ResolveModuleTenantIdentity( + ctx context.Context, req *gen.ResolveModuleTenantIdentityRequest, +) (*gen.ResolveModuleTenantIdentityResponse, error) { + if err := Validate(req); err != nil { + return nil, err + } + actorID, err := requireAuth(ctx) + if err != nil { + return nil, err + } + if err := requirePlatformRole(ctx, actorID, "super_admin"); err != nil { + return nil, err + } + resolution, err := service.ResolveModuleTenantIdentity(ctx, req.GetPrefix(), req.GetBinding(), req.GetOrganizationId()) + if err != nil { + return nil, translateGRPCError(err) + } + response := &gen.ResolveModuleTenantIdentityResponse{ + Refusal: resolution.Refusal, + Recent: make([]*gen.ModuleTenantIdentityMint, 0, len(resolution.Recent)), + } + if identity := resolution.Identity; identity != nil { + response.Identity = &gen.ModuleTenantIdentity{ + PrincipalId: identity.PrincipalID, + Tenant: identity.Tenant, + Audience: identity.Audience, + Scopes: identity.Scopes, + LifetimeSeconds: int64(identity.Lifetime / time.Second), + Revision: identity.Revision, + Shared: identity.Shared, + } + } + for _, mint := range resolution.Recent { + response.Recent = append(response.Recent, &gen.ModuleTenantIdentityMint{ + Record: mint.Record, + Tenant: mint.Tenant, + Binding: mint.Binding, + Audience: mint.Audience, + Scopes: mint.Scopes, + MintedAt: timestamppb.New(mint.MintedAt), + }) + } + return response, nil +} + +func (h *platformAdminConnectHandler) ResolveModuleTenantIdentity( + ctx context.Context, req *connect.Request[gen.ResolveModuleTenantIdentityRequest], +) (*connect.Response[gen.ResolveModuleTenantIdentityResponse], error) { + return unary(ctx, req, h.inner.ResolveModuleTenantIdentity) +} diff --git a/module/services/accounts/code/pkg/adapters/rest_registration_catalog_gen.go b/module/services/accounts/code/pkg/adapters/rest_registration_catalog_gen.go index de22597cd..6a494434e 100644 --- a/module/services/accounts/code/pkg/adapters/rest_registration_catalog_gen.go +++ b/module/services/accounts/code/pkg/adapters/rest_registration_catalog_gen.go @@ -143,6 +143,7 @@ var catalogRESTExactRoutes = map[string]struct{}{ "GET /v1/platform/jobs": {}, "GET /v1/platform/jobs/operations": {}, "GET /v1/platform/module-envelope": {}, + "GET /v1/platform/module-tenant-identity": {}, "GET /v1/platform/sessions": {}, "GET /v1/platform/users": {}, "GET /v1/platform/waitlist": {}, diff --git a/module/services/accounts/code/pkg/business/introspection.go b/module/services/accounts/code/pkg/business/introspection.go index 240503d70..f350aaa7c 100644 --- a/module/services/accounts/code/pkg/business/introspection.go +++ b/module/services/accounts/code/pkg/business/introspection.go @@ -276,6 +276,7 @@ var rpcDescriptions = withWorkContextConsumerDescriptions(map[string]string{ "PlatformAdminService/ListPlatformCatalogue": "List every composed module and solution with its declared, running and installed state.", "PlatformAdminService/GetModuleEnvelope": "Read the current module envelope, the authority ceiling every module principal is derived inside.", "PlatformAdminService/PutModuleEnvelope": "Write the next revision of the module envelope; it takes effect at the next check.", + "PlatformAdminService/ResolveModuleTenantIdentity": "Read what a per-tenant module operation context would assert for one module, binding and organization, or why it would be refused.", "PlatformAdminService/StopImpersonation": "End the caller's own impersonation session.", "PlatformAdminService/SuspendUser": "Suspend a user account.", "PlatformAdminService/UnsuspendUser": "Restore a suspended user.", diff --git a/module/services/accounts/code/pkg/business/module_operation_context.go b/module/services/accounts/code/pkg/business/module_operation_context.go index 550ead5da..3fb924b68 100644 --- a/module/services/accounts/code/pkg/business/module_operation_context.go +++ b/module/services/accounts/code/pkg/business/module_operation_context.go @@ -55,6 +55,24 @@ func (a ModuleOperationContextAuthority) WireScopes() []*gen.WorkContextScope { // binding's headless_scopes and nothing else: invoke_scopes describe what the // module may do on a person's behalf and are never reused for work that has no // person, so a binding that declares no headless_scopes is refused (fail closed). +// +// IT IS ALSO THE PER-TENANT MODULE IDENTITY, and that is worth naming rather +// than leaving as an incidental parameter (issue #1121). requestedOrganization +// is the organization the capability acts FOR: its tenant is that +// organization, so a module can present itself as acting for a tenant instead +// of as itself, which is what a consumer admitting a publication for a tenant +// needs from a publisher that has no person present. +// +// Which organizations may be named is the envelope's to decide and never the +// caller's (moduleMintOrganization): a principal PINNED to a tenant may name +// that tenant and no other, and an unpinned principal declaring cross_tenant +// may name one organization per mint. Both are then held to that organization's +// own live installation, and the binding is re-read AFTER the installation +// scope narrows it (authorizeModuleCapabilityFromSnapshot) — so the scopes a +// per-tenant capability carries are that tenant's consent and not the +// deployment's, and an organization consenting to nothing leaves the binding +// with no headless scopes and is refused. The bound is per tenant, which is +// the whole point: one identity per deployment could not be. func (s *Service) ModuleAuthorizeOperationContext( ctx context.Context, prefix, serviceAccountToken, bindingID string, requestedOrganization ...string, ) (ModuleOperationContextAuthority, error) { diff --git a/module/services/accounts/code/pkg/business/module_tenant_identity.go b/module/services/accounts/code/pkg/business/module_tenant_identity.go new file mode 100644 index 000000000..a20ecb6e2 --- /dev/null +++ b/module/services/accounts/code/pkg/business/module_tenant_identity.go @@ -0,0 +1,186 @@ +package business + +import ( + "context" + "errors" + "slices" + "time" +) + +// What a per-tenant module identity would assert, read for an operator. +// +// A composed module acting with no person present mints a Work Context for one +// of its installed operation bindings and names the organization it acts FOR +// (ModuleAuthorizeOperationContext). That capability is the per-tenant module +// identity: its tenant is the organization named, its scopes are that +// organization's installation narrowed to the binding's headless_scopes, it +// lives ModuleOperationContextTTL, and every issuance is recorded. +// +// This answers what that mint WOULD assert, and issues nothing. The mint needs +// the module pod's projected ServiceAccount token held against the build its +// authority document approves, which an operator's browser does not have and +// must never be given a way around: a path that handed an operator a module's +// capability would be a second spelling of that module's identity. So the +// resolution deliberately stops one step short of the signature, and takes the +// same decision from the same declared facts the mint takes it from — not a +// second copy of the rule, because every refusal below is the mint's own +// function answering. + +// ModuleTenantIdentity is what one per-tenant mint would assert. +type ModuleTenantIdentity struct { + PrincipalID string + Tenant string + Audience string + // Scopes is what the capability would carry, as `kind:action[:resource]`. + Scopes []string + Lifetime time.Duration + Revision uint64 + // Shared says the principal is unpinned and declares cross_tenant, so it + // may act for more than one organization. A pinned principal can only ever + // name the tenant its envelope declares. + Shared bool +} + +// ModuleTenantIdentityMint is one capability actually issued, from the trail +// that records every mint. There is no other record of one. +type ModuleTenantIdentityMint struct { + Record string + Tenant string + Binding string + Audience string + Scopes []string + MintedAt time.Time +} + +// ModuleTenantIdentityResolution is the answer: either what would be minted, or +// why nothing would be. +type ModuleTenantIdentityResolution struct { + Identity *ModuleTenantIdentity + Refusal string + Recent []ModuleTenantIdentityMint +} + +// The refusals, in the operator's words rather than as an error code. Each one +// is a decision about declared authority — never an outage to retry. +const ( + // ModuleTenantIdentityUndeclared: no such module principal is derived from + // the current envelope, so nothing can be minted under that prefix at all. + ModuleTenantIdentityUndeclared = "no module principal is declared under this prefix" + // ModuleTenantIdentityTenantRefused: the envelope pins this principal to + // another tenant, or leaves it unpinned without cross_tenant — either way + // it may not act for the organization named. + ModuleTenantIdentityTenantRefused = "this module may not act for that organization: its envelope pins it elsewhere, or does not declare cross_tenant" + // ModuleTenantIdentityNotInstalled: the organization has no live + // installation consenting to this module. + ModuleTenantIdentityNotInstalled = "the organization has no live installation of this module" + // ModuleTenantIdentityBindingRefused: the binding is not declared, or + // declares no headless_scopes, or this organization's installation scope + // leaves it with none — the last being the per-tenant bound doing its job. + ModuleTenantIdentityBindingRefused = "this binding is not mintable for that organization with no person present: it declares no headless scopes, or the organization's installation consents to none of them" +) + +// ResolveModuleTenantIdentity answers what a per-tenant module operation +// context would assert for one module, one of its bindings and one +// organization — or why it would be refused — and the capabilities that module +// was actually issued for that organization. +// +// The decision order is the mint's, and each step answers a different question +// an operator asks: is the principal declared at all, may it act for this +// organization, does this organization install it, and does the binding confer +// anything headless HERE. Collapsing them into one refusal would answer none +// of them. +func (s *Service) ResolveModuleTenantIdentity( + ctx context.Context, prefix, bindingID, orgID string, +) (ModuleTenantIdentityResolution, error) { + if prefix == "" || bindingID == "" || orgID == "" { + return ModuleTenantIdentityResolution{}, errors.New("module prefix, binding and organization are required") + } + ctx, releaseAuthority, fenceErr := s.BeginModuleAuthorityUse(ctx) + if fenceErr != nil { + return ModuleTenantIdentityResolution{}, fenceErr + } + defer releaseAuthority() + + // The trail is read whatever the answer: an operator looking at a module + // that may no longer act for an organization still needs to see what it was + // issued while it could. + recent, err := s.moduleTenantIdentityMints(ctx, prefix, orgID) + if err != nil { + return ModuleTenantIdentityResolution{}, err + } + resolution := ModuleTenantIdentityResolution{Recent: recent} + + snapshot := s.moduleRegistrySnapshot() + principalID := ModulePrincipalID(prefix) + declared, isModule := snapshot.registry[principalID] + if !isModule || declared.Prefix == "" { + resolution.Refusal = ModuleTenantIdentityUndeclared + return resolution, nil + } + // The envelope's own rule about which organizations this principal may + // name, asked of the function the mint asks (never a second reading of + // `tenant` and `cross_tenant` here). + if _, err := moduleMintOrganization(declared, []string{orgID}); err != nil { + resolution.Refusal = ModuleTenantIdentityTenantRefused + return resolution, nil + } + installed, _, err := s.authorizeModuleCapabilityFromSnapshot(ctx, snapshot, + ModuleCaller{PrincipalID: principalID, BoundOrg: orgID}) + if err != nil { + // A module the organization does not install is a declared refusal, not + // a failure of this read; anything else (an unreadable envelope + // revision, a fence) is the caller's to see as it is. + if errors.Is(err, ErrModuleInstallationInactive) || errors.Is(err, ErrModuleTenantNotCurrent) { + resolution.Refusal = ModuleTenantIdentityNotInstalled + return resolution, nil + } + return ModuleTenantIdentityResolution{}, err + } + // AFTER the installation scope narrowed it. A binding with headless scopes + // the deployment declares but this organization did not consent to has none + // here, and that is the per-tenant bound rather than an edge case. + grant := installed.Grant + binding, bound := grant.OperationAudiences[bindingID] + if !bound || len(binding.HeadlessScopes) == 0 || + validateOperationAudiences(grant.Prefix, map[string]ModuleOperationAudience{bindingID: binding}) != nil { + resolution.Refusal = ModuleTenantIdentityBindingRefused + return resolution, nil + } + resolution.Identity = &ModuleTenantIdentity{ + PrincipalID: principalID, + Tenant: orgID, + Audience: binding.Audience, + Scopes: operationScopeGrants(binding.HeadlessScopes), + Lifetime: ModuleOperationContextTTL, + Revision: ModuleOperationContextRevision(grant), + Shared: declared.Tenant == "" && declared.CrossTenant, + } + return resolution, nil +} + +// moduleTenantIdentityMints reads the capabilities one module was issued for +// one organization, newest first, from the trail that records every mint. +func (s *Service) moduleTenantIdentityMints( + ctx context.Context, prefix, orgID string, +) ([]ModuleTenantIdentityMint, error) { + entries, _, _, err := s.QueryAuditLog(ctx, AuditQuery{ + OrgID: orgID, + EventType: string(EventModuleOperationContextMint), + Resource: "module", + ResourceID: prefix, + PageSize: 25, + }) + if err != nil { + return nil, err + } + mints := make([]ModuleTenantIdentityMint, 0, len(entries)) + for _, entry := range entries { + mint := ModuleTenantIdentityMint{Record: entry.ID, MintedAt: entry.CreatedAt} + mint.Tenant, _ = entry.Payload["tenant"].(string) + mint.Binding, _ = entry.Payload["binding_id"].(string) + mint.Audience, _ = entry.Payload["audience"].(string) + mint.Scopes = slices.Clip(auditStringList(entry.Payload["scopes"])) + mints = append(mints, mint) + } + return mints, nil +} diff --git a/module/services/accounts/code/pkg/business/module_tenant_identity_test.go b/module/services/accounts/code/pkg/business/module_tenant_identity_test.go new file mode 100644 index 000000000..70870c9d4 --- /dev/null +++ b/module/services/accounts/code/pkg/business/module_tenant_identity_test.go @@ -0,0 +1,162 @@ +//go:build pure + +package business + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +// A MODULE IDENTITY MINTED FOR ONE TENANT, read for an operator (issue #1121). +// +// The capability itself is the headless operation mint naming an organization, +// and that it mints per tenant — with that tenant's own installation narrowing +// what it carries — is proved against the mint in +// TestSharedInstallationMintSelectsOnlyConsentingOrganization. What is tested +// here is the READ an operator drives the playground from: it must answer from +// the same declared facts the mint decides on, and it must issue nothing. +// +// The refusals are four separate answers because they are four different things +// an operator does about them: declare the principal, widen the envelope's +// tenancy, install the module in that organization, or consent to the binding's +// headless scopes there. One collapsed refusal would answer none of them. + +// perTenantIdentityFixture is the shared-principal deployment: one module +// unpinned and cross-tenant, installed in two organizations, whose binding +// declares headless scopes — and whose second organization consented to fewer +// of them. A third organization has no installation at all. +func perTenantIdentityFixture(t *testing.T) (*Service, string, string, string) { + t.Helper() + s, live, caller := dynamicInstallationFixture() + grant := s.ModulePrincipals()[caller.PrincipalID] + binding := grant.OperationAudiences["operate"] + binding.HeadlessScopes = cloneOperationScopes(binding.InvokeScopes) + grant.OperationAudiences["operate"] = binding + live.rows["org-a/principal"].Scope = FullInstallationScope(grant) + // The second organization's installation consents to a narrower binding, + // which is what makes the bound per tenant rather than per deployment. + narrowed := live.rows["org-b/principal"].Scope.OperationAudiences["operate"] + narrowed.HeadlessScopes = cloneOperationScopes(narrowed.InvokeScopes) + live.rows["org-b/principal"].Scope.OperationAudiences["operate"] = narrowed + + id := ModulePrincipalID(grant.Prefix) + first, second, third := uuid.NewString(), uuid.NewString(), uuid.NewString() + live.rows[first+"/"+id] = live.rows["org-a/principal"] + live.rows[second+"/"+id] = live.rows["org-b/principal"] + // Unpinned and cross-tenant: the shape a module that acts for many tenants + // is declared in, and the only one that may name an organization per mint. + grant.Tenant = "" + s.SetModulePrincipals(ModulePrincipalRegistry{id: grant}) + // The resolution also reads the trail of capabilities actually issued, in + // the organization's own transaction. The store is swapped rather than the + // read made optional: a read that swallowed its own failure would report + // "no capability was ever issued" over a database that could not answer. + s.store = perTenantIdentityStore{Store: s.store} + return s, first, second, third +} + +// perTenantIdentityStore answers the organization-scoped audit read with an +// empty trail. Empty rather than seeded: what the trail CONTAINS is the audit +// log's own contract, tested next door; what matters here is that the +// resolution asks for it inside the organization's transaction and does not +// invent a tenant for the question. +type perTenantIdentityStore struct{ Store } + +func (perTenantIdentityStore) WithOrgTx(ctx context.Context, _ string, fn func(context.Context) error) error { + return fn(ctx) +} + +func (perTenantIdentityStore) QueryAuditLog(context.Context, AuditQuery) ([]AuditEntry, string, int32, error) { + return nil, "", 0, nil +} + +func TestResolveModuleTenantIdentityAnswersWhatOneTenantsMintWouldAssert(t *testing.T) { + s, first, second, third := perTenantIdentityFixture(t) + ctx := context.Background() + + full, err := s.ResolveModuleTenantIdentity(ctx, "example", "operate", first) + require.NoError(t, err) + require.Empty(t, full.Refusal) + require.NotNil(t, full.Identity) + require.Equal(t, ModulePrincipalID("example"), full.Identity.PrincipalID) + require.Equal(t, first, full.Identity.Tenant, "the capability acts FOR the organization named") + require.Equal(t, "example.api", full.Identity.Audience) + require.Equal(t, ModuleOperationContextTTL, full.Identity.Lifetime) + require.NotZero(t, full.Identity.Revision) + require.True(t, full.Identity.Shared, "an unpinned cross-tenant principal may act for more than one organization") + require.Equal(t, []string{ + "records:read:one", "records:read:two", "records:write:one", "records:write:two", + }, full.Identity.Scopes) + + // THE SAME MODULE, THE SAME BINDING, A DIFFERENT TENANT: fewer scopes, + // because that organization's installation consented to fewer. This is the + // property a single deployment-wide identity cannot have. + narrowed, err := s.ResolveModuleTenantIdentity(ctx, "example", "operate", second) + require.NoError(t, err) + require.NotNil(t, narrowed.Identity) + require.Equal(t, second, narrowed.Identity.Tenant) + require.Equal(t, []string{"records:read:one"}, narrowed.Identity.Scopes) + require.NotEqual(t, full.Identity.Scopes, narrowed.Identity.Scopes, + "the two tenants' capabilities carried the same authority, so nothing is bounded per tenant") + + // An organization with no live installation: the module may act for a + // tenant only where that tenant installed it. + absent, err := s.ResolveModuleTenantIdentity(ctx, "example", "operate", third) + require.NoError(t, err) + require.Nil(t, absent.Identity) + require.Equal(t, ModuleTenantIdentityNotInstalled, absent.Refusal) + + // A binding that is not declared, and one that declares no headless + // scopes: neither is mintable with no person present. + undeclared, err := s.ResolveModuleTenantIdentity(ctx, "example", "absent", first) + require.NoError(t, err) + require.Equal(t, ModuleTenantIdentityBindingRefused, undeclared.Refusal) + + // A prefix no module principal is declared under. + unknown, err := s.ResolveModuleTenantIdentity(ctx, "nobody", "operate", first) + require.NoError(t, err) + require.Equal(t, ModuleTenantIdentityUndeclared, unknown.Refusal) + + // And the arguments it cannot answer without. + _, err = s.ResolveModuleTenantIdentity(ctx, "", "operate", first) + require.Error(t, err) + _, err = s.ResolveModuleTenantIdentity(ctx, "example", "operate", "") + require.Error(t, err) +} + +// A PINNED PRINCIPAL MAY ACT FOR ITS OWN TENANT AND NO OTHER. +// +// The pin is the bound, so the refusal for another organization is the +// envelope's decision and not a missing feature: a module that should act for +// many tenants is declared unpinned and cross-tenant, which is the fixture +// above. Reported as its own refusal because what an operator does about it — +// change the envelope — is not what they do about a missing installation. +func TestResolveModuleTenantIdentityRefusesATenantThePinForbids(t *testing.T) { + s, first, second, _ := perTenantIdentityFixture(t) + grant := s.ModulePrincipals()[ModulePrincipalID("example")] + grant.Tenant = first + s.SetModulePrincipals(ModulePrincipalRegistry{ModulePrincipalID("example"): grant}) + ctx := context.Background() + + own, err := s.ResolveModuleTenantIdentity(ctx, "example", "operate", first) + require.NoError(t, err) + require.NotNil(t, own.Identity) + require.False(t, own.Identity.Shared, "a pinned principal acts for one organization, and the read says so") + + other, err := s.ResolveModuleTenantIdentity(ctx, "example", "operate", second) + require.NoError(t, err) + require.Nil(t, other.Identity) + require.Equal(t, ModuleTenantIdentityTenantRefused, other.Refusal) + + // Unpinned but NOT cross-tenant is refused the same way: naming an + // organization is not authority over it, and a shared mint needs the + // declaration that says so. + grant.Tenant, grant.CrossTenant = "", false + s.SetModulePrincipals(ModulePrincipalRegistry{ModulePrincipalID("example"): grant}) + neither, err := s.ResolveModuleTenantIdentity(ctx, "example", "operate", first) + require.NoError(t, err) + require.Equal(t, ModuleTenantIdentityTenantRefused, neither.Refusal) +} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go index c6525c822..ed8a50363 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go @@ -112,6 +112,9 @@ const ( // PlatformAdminServicePutModuleEnvelopeProcedure is the fully-qualified name of the // PlatformAdminService's PutModuleEnvelope RPC. PlatformAdminServicePutModuleEnvelopeProcedure = "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope" + // PlatformAdminServiceResolveModuleTenantIdentityProcedure is the fully-qualified name of the + // PlatformAdminService's ResolveModuleTenantIdentity RPC. + PlatformAdminServiceResolveModuleTenantIdentityProcedure = "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity" ) // PlatformAdminServiceClient is a client for the saas.accounts.v1.PlatformAdminService service. @@ -174,6 +177,10 @@ type PlatformAdminServiceClient interface { // effect at the next check with no restart: the module registry is derived // again from it, so a narrowing removes what the new ceiling no longer allows. PutModuleEnvelope(context.Context, *connect.Request[v1.PutModuleEnvelopeRequest]) (*connect.Response[v1.PutModuleEnvelopeResponse], error) + // ResolveModuleTenantIdentity answers what a per-tenant module operation + // context would assert for one module, one of its bindings and one + // organization — or why it would be refused. It issues no capability. + ResolveModuleTenantIdentity(context.Context, *connect.Request[v1.ResolveModuleTenantIdentityRequest]) (*connect.Response[v1.ResolveModuleTenantIdentityResponse], error) } // NewPlatformAdminServiceClient constructs a client for the saas.accounts.v1.PlatformAdminService @@ -337,36 +344,43 @@ func NewPlatformAdminServiceClient(httpClient connect.HTTPClient, baseURL string connect.WithSchema(platformAdminServiceMethods.ByName("PutModuleEnvelope")), connect.WithClientOptions(opts...), ), + resolveModuleTenantIdentity: connect.NewClient[v1.ResolveModuleTenantIdentityRequest, v1.ResolveModuleTenantIdentityResponse]( + httpClient, + baseURL+PlatformAdminServiceResolveModuleTenantIdentityProcedure, + connect.WithSchema(platformAdminServiceMethods.ByName("ResolveModuleTenantIdentity")), + connect.WithClientOptions(opts...), + ), } } // platformAdminServiceClient implements PlatformAdminServiceClient. type platformAdminServiceClient struct { - searchUsers *connect.Client[v1.SearchUsersRequest, v1.SearchUsersResponse] - suspendUser *connect.Client[v1.SuspendUserRequest, emptypb.Empty] - unsuspendUser *connect.Client[v1.UnsuspendUserRequest, emptypb.Empty] - impersonateUser *connect.Client[v1.ImpersonateUserRequest, v1.ImpersonateUserResponse] - stopImpersonation *connect.Client[v1.StopImpersonationRequest, v1.StopImpersonationResponse] - listActiveSessions *connect.Client[v1.ListActiveSessionsRequest, v1.ListActiveSessionsResponse] - revokeSession *connect.Client[v1.RevokeSessionRequest, emptypb.Empty] - getOrgEntitlements *connect.Client[v1.GetOrgEntitlementsRequest, v1.GetOrgEntitlementsResponse] - overrideEntitlement *connect.Client[v1.OverrideEntitlementRequest, v1.OverrideEntitlementResponse] - grantPlatformRole *connect.Client[v1.GrantPlatformRoleRequest, emptypb.Empty] - revokePlatformRole *connect.Client[v1.RevokePlatformRoleRequest, emptypb.Empty] - listPlatformAdmins *connect.Client[v1.ListPlatformAdminsRequest, v1.ListPlatformAdminsResponse] - listAllOrganizations *connect.Client[v1.ListAllOrganizationsRequest, v1.ListAllOrganizationsResponse] - getOrganizationRoster *connect.Client[v1.GetOrganizationRosterRequest, v1.GetOrganizationRosterResponse] - listFeatureFlags *connect.Client[v1.ListFeatureFlagsRequest, v1.ListFeatureFlagsResponse] - upsertFeatureFlag *connect.Client[v1.UpsertFeatureFlagRequest, v1.UpsertFeatureFlagResponse] - getJobOperations *connect.Client[v11.GetJobOperationsRequest, v11.GetJobOperationsResponse] - listJobs *connect.Client[v11.ListJobsRequest, v11.ListJobsResponse] - getJob *connect.Client[v11.GetJobRequest, v11.GetJobResponse] - replayJob *connect.Client[v11.ReplayJobRequest, v11.ReplayJobResponse] - getEventOperations *connect.Client[v12.GetEventOperationsRequest, v12.GetEventOperationsResponse] - listEventSubscriptions *connect.Client[v12.ListEventSubscriptionsRequest, v12.ListEventSubscriptionsResponse] - listPlatformCatalogue *connect.Client[v1.ListPlatformCatalogueRequest, v1.ListPlatformCatalogueResponse] - getModuleEnvelope *connect.Client[v1.GetModuleEnvelopeRequest, v1.GetModuleEnvelopeResponse] - putModuleEnvelope *connect.Client[v1.PutModuleEnvelopeRequest, v1.PutModuleEnvelopeResponse] + searchUsers *connect.Client[v1.SearchUsersRequest, v1.SearchUsersResponse] + suspendUser *connect.Client[v1.SuspendUserRequest, emptypb.Empty] + unsuspendUser *connect.Client[v1.UnsuspendUserRequest, emptypb.Empty] + impersonateUser *connect.Client[v1.ImpersonateUserRequest, v1.ImpersonateUserResponse] + stopImpersonation *connect.Client[v1.StopImpersonationRequest, v1.StopImpersonationResponse] + listActiveSessions *connect.Client[v1.ListActiveSessionsRequest, v1.ListActiveSessionsResponse] + revokeSession *connect.Client[v1.RevokeSessionRequest, emptypb.Empty] + getOrgEntitlements *connect.Client[v1.GetOrgEntitlementsRequest, v1.GetOrgEntitlementsResponse] + overrideEntitlement *connect.Client[v1.OverrideEntitlementRequest, v1.OverrideEntitlementResponse] + grantPlatformRole *connect.Client[v1.GrantPlatformRoleRequest, emptypb.Empty] + revokePlatformRole *connect.Client[v1.RevokePlatformRoleRequest, emptypb.Empty] + listPlatformAdmins *connect.Client[v1.ListPlatformAdminsRequest, v1.ListPlatformAdminsResponse] + listAllOrganizations *connect.Client[v1.ListAllOrganizationsRequest, v1.ListAllOrganizationsResponse] + getOrganizationRoster *connect.Client[v1.GetOrganizationRosterRequest, v1.GetOrganizationRosterResponse] + listFeatureFlags *connect.Client[v1.ListFeatureFlagsRequest, v1.ListFeatureFlagsResponse] + upsertFeatureFlag *connect.Client[v1.UpsertFeatureFlagRequest, v1.UpsertFeatureFlagResponse] + getJobOperations *connect.Client[v11.GetJobOperationsRequest, v11.GetJobOperationsResponse] + listJobs *connect.Client[v11.ListJobsRequest, v11.ListJobsResponse] + getJob *connect.Client[v11.GetJobRequest, v11.GetJobResponse] + replayJob *connect.Client[v11.ReplayJobRequest, v11.ReplayJobResponse] + getEventOperations *connect.Client[v12.GetEventOperationsRequest, v12.GetEventOperationsResponse] + listEventSubscriptions *connect.Client[v12.ListEventSubscriptionsRequest, v12.ListEventSubscriptionsResponse] + listPlatformCatalogue *connect.Client[v1.ListPlatformCatalogueRequest, v1.ListPlatformCatalogueResponse] + getModuleEnvelope *connect.Client[v1.GetModuleEnvelopeRequest, v1.GetModuleEnvelopeResponse] + putModuleEnvelope *connect.Client[v1.PutModuleEnvelopeRequest, v1.PutModuleEnvelopeResponse] + resolveModuleTenantIdentity *connect.Client[v1.ResolveModuleTenantIdentityRequest, v1.ResolveModuleTenantIdentityResponse] } // SearchUsers calls saas.accounts.v1.PlatformAdminService.SearchUsers. @@ -496,6 +510,12 @@ func (c *platformAdminServiceClient) PutModuleEnvelope(ctx context.Context, req return c.putModuleEnvelope.CallUnary(ctx, req) } +// ResolveModuleTenantIdentity calls +// saas.accounts.v1.PlatformAdminService.ResolveModuleTenantIdentity. +func (c *platformAdminServiceClient) ResolveModuleTenantIdentity(ctx context.Context, req *connect.Request[v1.ResolveModuleTenantIdentityRequest]) (*connect.Response[v1.ResolveModuleTenantIdentityResponse], error) { + return c.resolveModuleTenantIdentity.CallUnary(ctx, req) +} + // PlatformAdminServiceHandler is an implementation of the saas.accounts.v1.PlatformAdminService // service. type PlatformAdminServiceHandler interface { @@ -557,6 +577,10 @@ type PlatformAdminServiceHandler interface { // effect at the next check with no restart: the module registry is derived // again from it, so a narrowing removes what the new ceiling no longer allows. PutModuleEnvelope(context.Context, *connect.Request[v1.PutModuleEnvelopeRequest]) (*connect.Response[v1.PutModuleEnvelopeResponse], error) + // ResolveModuleTenantIdentity answers what a per-tenant module operation + // context would assert for one module, one of its bindings and one + // organization — or why it would be refused. It issues no capability. + ResolveModuleTenantIdentity(context.Context, *connect.Request[v1.ResolveModuleTenantIdentityRequest]) (*connect.Response[v1.ResolveModuleTenantIdentityResponse], error) } // NewPlatformAdminServiceHandler builds an HTTP handler from the service implementation. It returns @@ -716,6 +740,12 @@ func NewPlatformAdminServiceHandler(svc PlatformAdminServiceHandler, opts ...con connect.WithSchema(platformAdminServiceMethods.ByName("PutModuleEnvelope")), connect.WithHandlerOptions(opts...), ) + platformAdminServiceResolveModuleTenantIdentityHandler := connect.NewUnaryHandler( + PlatformAdminServiceResolveModuleTenantIdentityProcedure, + svc.ResolveModuleTenantIdentity, + connect.WithSchema(platformAdminServiceMethods.ByName("ResolveModuleTenantIdentity")), + connect.WithHandlerOptions(opts...), + ) return "/saas.accounts.v1.PlatformAdminService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case PlatformAdminServiceSearchUsersProcedure: @@ -768,6 +798,8 @@ func NewPlatformAdminServiceHandler(svc PlatformAdminServiceHandler, opts ...con platformAdminServiceGetModuleEnvelopeHandler.ServeHTTP(w, r) case PlatformAdminServicePutModuleEnvelopeProcedure: platformAdminServicePutModuleEnvelopeHandler.ServeHTTP(w, r) + case PlatformAdminServiceResolveModuleTenantIdentityProcedure: + platformAdminServiceResolveModuleTenantIdentityHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -876,3 +908,7 @@ func (UnimplementedPlatformAdminServiceHandler) GetModuleEnvelope(context.Contex func (UnimplementedPlatformAdminServiceHandler) PutModuleEnvelope(context.Context, *connect.Request[v1.PutModuleEnvelopeRequest]) (*connect.Response[v1.PutModuleEnvelopeResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.PlatformAdminService.PutModuleEnvelope is not implemented")) } + +func (UnimplementedPlatformAdminServiceHandler) ResolveModuleTenantIdentity(context.Context, *connect.Request[v1.ResolveModuleTenantIdentityRequest]) (*connect.Response[v1.ResolveModuleTenantIdentityResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.PlatformAdminService.ResolveModuleTenantIdentity is not implemented")) +} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go index e6eaf12db..06c099f99 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go @@ -3751,6 +3751,332 @@ func (x *PutModuleEnvelopeResponse) GetEnvelope() *ModuleEnvelope { return nil } +type ResolveModuleTenantIdentityRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // prefix is the module principal's registration prefix and binding the key + // of its operation_audiences entry. + Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` + Binding string `protobuf:"bytes,2,opt,name=binding,proto3" json:"binding,omitempty"` + // organization_id is the tenant the capability would act for. + OrganizationId string `protobuf:"bytes,3,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolveModuleTenantIdentityRequest) Reset() { + *x = ResolveModuleTenantIdentityRequest{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolveModuleTenantIdentityRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolveModuleTenantIdentityRequest) ProtoMessage() {} + +func (x *ResolveModuleTenantIdentityRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolveModuleTenantIdentityRequest.ProtoReflect.Descriptor instead. +func (*ResolveModuleTenantIdentityRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{52} +} + +func (x *ResolveModuleTenantIdentityRequest) GetPrefix() string { + if x != nil { + return x.Prefix + } + return "" +} + +func (x *ResolveModuleTenantIdentityRequest) GetBinding() string { + if x != nil { + return x.Binding + } + return "" +} + +func (x *ResolveModuleTenantIdentityRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +// ModuleTenantIdentity is what one per-tenant mint would assert. Every field is +// the host's: nothing here is requestable. +type ModuleTenantIdentity struct { + state protoimpl.MessageState `protogen:"open.v1"` + // principal_id is the module's own service principal, which owns the + // capability and acts directly — there is no person and no actor hop. + PrincipalId string `protobuf:"bytes,1,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty"` + // tenant is the organization it would act for, and audience the service the + // binding declares it may call. A binding whose audience is the module's own + // prefix is one the module admits itself, which is how unattended work is + // admitted by the module that executes it. + Tenant string `protobuf:"bytes,2,opt,name=tenant,proto3" json:"tenant,omitempty"` + Audience string `protobuf:"bytes,3,opt,name=audience,proto3" json:"audience,omitempty"` + // scopes is exactly what the capability would carry, as + // `kind:action[:resource]`: the binding's headless_scopes narrowed by THIS + // organization's installation scope. Never its invoke_scopes, which describe + // authority exercised on a person's behalf. + Scopes []string `protobuf:"bytes,4,rep,name=scopes,proto3" json:"scopes,omitempty"` + // lifetime_seconds is the fixed lifetime every such capability carries. + LifetimeSeconds int64 `protobuf:"varint,5,opt,name=lifetime_seconds,json=lifetimeSeconds,proto3" json:"lifetime_seconds,omitempty"` + // revision is the authorization revision it would be sealed with: a digest + // of the module's whole effective entry, so any change to the entry stops + // confirming every capability minted under the old one. + Revision uint64 `protobuf:"varint,6,opt,name=revision,proto3" json:"revision,omitempty"` + // shared says the principal is unpinned and declares cross_tenant, so it may + // act for more than one organization. A pinned principal reads false and can + // only ever name the tenant its envelope declares. + Shared bool `protobuf:"varint,7,opt,name=shared,proto3" json:"shared,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleTenantIdentity) Reset() { + *x = ModuleTenantIdentity{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleTenantIdentity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleTenantIdentity) ProtoMessage() {} + +func (x *ModuleTenantIdentity) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModuleTenantIdentity.ProtoReflect.Descriptor instead. +func (*ModuleTenantIdentity) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{53} +} + +func (x *ModuleTenantIdentity) GetPrincipalId() string { + if x != nil { + return x.PrincipalId + } + return "" +} + +func (x *ModuleTenantIdentity) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *ModuleTenantIdentity) GetAudience() string { + if x != nil { + return x.Audience + } + return "" +} + +func (x *ModuleTenantIdentity) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *ModuleTenantIdentity) GetLifetimeSeconds() int64 { + if x != nil { + return x.LifetimeSeconds + } + return 0 +} + +func (x *ModuleTenantIdentity) GetRevision() uint64 { + if x != nil { + return x.Revision + } + return 0 +} + +func (x *ModuleTenantIdentity) GetShared() bool { + if x != nil { + return x.Shared + } + return false +} + +// ModuleTenantIdentityMint is one capability actually issued, as the audit +// trail recorded it. There is no other record: a capability lives a minute and +// nothing caches one. +type ModuleTenantIdentityMint struct { + state protoimpl.MessageState `protogen:"open.v1"` + Record string `protobuf:"bytes,1,opt,name=record,proto3" json:"record,omitempty"` + Tenant string `protobuf:"bytes,2,opt,name=tenant,proto3" json:"tenant,omitempty"` + Binding string `protobuf:"bytes,3,opt,name=binding,proto3" json:"binding,omitempty"` + Audience string `protobuf:"bytes,4,opt,name=audience,proto3" json:"audience,omitempty"` + Scopes []string `protobuf:"bytes,5,rep,name=scopes,proto3" json:"scopes,omitempty"` + MintedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=minted_at,json=mintedAt,proto3" json:"minted_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleTenantIdentityMint) Reset() { + *x = ModuleTenantIdentityMint{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleTenantIdentityMint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleTenantIdentityMint) ProtoMessage() {} + +func (x *ModuleTenantIdentityMint) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModuleTenantIdentityMint.ProtoReflect.Descriptor instead. +func (*ModuleTenantIdentityMint) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{54} +} + +func (x *ModuleTenantIdentityMint) GetRecord() string { + if x != nil { + return x.Record + } + return "" +} + +func (x *ModuleTenantIdentityMint) GetTenant() string { + if x != nil { + return x.Tenant + } + return "" +} + +func (x *ModuleTenantIdentityMint) GetBinding() string { + if x != nil { + return x.Binding + } + return "" +} + +func (x *ModuleTenantIdentityMint) GetAudience() string { + if x != nil { + return x.Audience + } + return "" +} + +func (x *ModuleTenantIdentityMint) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *ModuleTenantIdentityMint) GetMintedAt() *timestamppb.Timestamp { + if x != nil { + return x.MintedAt + } + return nil +} + +type ResolveModuleTenantIdentityResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // identity is set when a mint for this organization would be issued; refusal + // says why it would not. Exactly one is present, and a refusal is a decision + // about declared authority rather than an outage. + Identity *ModuleTenantIdentity `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` + Refusal string `protobuf:"bytes,2,opt,name=refusal,proto3" json:"refusal,omitempty"` + // recent is the trail of capabilities this module was actually issued for + // this organization, newest first. + Recent []*ModuleTenantIdentityMint `protobuf:"bytes,3,rep,name=recent,proto3" json:"recent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolveModuleTenantIdentityResponse) Reset() { + *x = ResolveModuleTenantIdentityResponse{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolveModuleTenantIdentityResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolveModuleTenantIdentityResponse) ProtoMessage() {} + +func (x *ResolveModuleTenantIdentityResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolveModuleTenantIdentityResponse.ProtoReflect.Descriptor instead. +func (*ResolveModuleTenantIdentityResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{55} +} + +func (x *ResolveModuleTenantIdentityResponse) GetIdentity() *ModuleTenantIdentity { + if x != nil { + return x.Identity + } + return nil +} + +func (x *ResolveModuleTenantIdentityResponse) GetRefusal() string { + if x != nil { + return x.Refusal + } + return "" +} + +func (x *ResolveModuleTenantIdentityResponse) GetRecent() []*ModuleTenantIdentityMint { + if x != nil { + return x.Recent + } + return nil +} + var File_saas_accounts_v1_platform_admin_proto protoreflect.FileDescriptor const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + @@ -3993,7 +4319,31 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\x11expected_revision\x18\x01 \x01(\x03B\a\xbaH\x04\"\x02(\x00R\x10expectedRevision\x12'\n" + "\bdocument\x18\x02 \x01(\tB\v\xbaH\br\x06\x10\x01(\x80\x80@R\bdocument\"Y\n" + "\x19PutModuleEnvelopeResponse\x12<\n" + - "\benvelope\x18\x01 \x01(\v2 .saas.accounts.v1.ModuleEnvelopeR\benvelope*~\n" + + "\benvelope\x18\x01 \x01(\v2 .saas.accounts.v1.ModuleEnvelopeR\benvelope\"\xc3\x01\n" + + "\"ResolveModuleTenantIdentityRequest\x12D\n" + + "\x06prefix\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$R\x06prefix\x12$\n" + + "\abinding\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\abinding\x121\n" + + "\x0forganization_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0eorganizationId\"\xe4\x01\n" + + "\x14ModuleTenantIdentity\x12!\n" + + "\fprincipal_id\x18\x01 \x01(\tR\vprincipalId\x12\x16\n" + + "\x06tenant\x18\x02 \x01(\tR\x06tenant\x12\x1a\n" + + "\baudience\x18\x03 \x01(\tR\baudience\x12\x16\n" + + "\x06scopes\x18\x04 \x03(\tR\x06scopes\x12)\n" + + "\x10lifetime_seconds\x18\x05 \x01(\x03R\x0flifetimeSeconds\x12\x1a\n" + + "\brevision\x18\x06 \x01(\x04R\brevision\x12\x16\n" + + "\x06shared\x18\a \x01(\bR\x06shared\"\xd1\x01\n" + + "\x18ModuleTenantIdentityMint\x12\x16\n" + + "\x06record\x18\x01 \x01(\tR\x06record\x12\x16\n" + + "\x06tenant\x18\x02 \x01(\tR\x06tenant\x12\x18\n" + + "\abinding\x18\x03 \x01(\tR\abinding\x12\x1a\n" + + "\baudience\x18\x04 \x01(\tR\baudience\x12\x16\n" + + "\x06scopes\x18\x05 \x03(\tR\x06scopes\x127\n" + + "\tminted_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\bmintedAt\"\xc7\x01\n" + + "#ResolveModuleTenantIdentityResponse\x12B\n" + + "\bidentity\x18\x01 \x01(\v2&.saas.accounts.v1.ModuleTenantIdentityR\bidentity\x12\x18\n" + + "\arefusal\x18\x02 \x01(\tR\arefusal\x12B\n" + + "\x06recent\x18\x03 \x03(\v2*.saas.accounts.v1.ModuleTenantIdentityMintR\x06recent*~\n" + "\x12CatalogueEntryKind\x12$\n" + " CATALOGUE_ENTRY_KIND_UNSPECIFIED\x10\x00\x12\x1f\n" + "\x1bCATALOGUE_ENTRY_KIND_MODULE\x10\x01\x12!\n" + @@ -4007,7 +4357,7 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "&CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED\x10\x00\x121\n" + "-CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED\x10\x01\x12.\n" + "*CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS\x10\x02\x123\n" + - "/CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED\x10\x032\xf5\"\n" + + "/CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED\x10\x032\xc7$\n" + "\x14PlatformAdminService\x12\x8e\x01\n" + "\vSearchUsers\x12$.saas.accounts.v1.SearchUsersRequest\x1a%.saas.accounts.v1.SearchUsersResponse\"2\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/platform/users\x12\xab\x01\n" + "\vSuspendUser\x12$.saas.accounts.v1.SuspendUserRequest\x1a\x16.google.protobuf.Empty\"^\xc2\xf3\x18+\b\x02\x10\x010\x01:\x17\n" + @@ -4045,7 +4395,8 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\x15ListPlatformCatalogue\x12..saas.accounts.v1.ListPlatformCatalogueRequest\x1a/.saas.accounts.v1.ListPlatformCatalogueResponse\"6\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x18\x12\x16/v1/platform/catalogue\x12\xaa\x01\n" + "\x11GetModuleEnvelope\x12*.saas.accounts.v1.GetModuleEnvelopeRequest\x1a+.saas.accounts.v1.GetModuleEnvelopeResponse\"<\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/platform/module-envelope\x12\xd6\x01\n" + "\x11PutModuleEnvelope\x12*.saas.accounts.v1.PutModuleEnvelopeRequest\x1a+.saas.accounts.v1.PutModuleEnvelopeResponse\"h\xc2\xf3\x18=\b\x02\x10\x010\x03:)\n" + - "%saas.platform.module_envelope_written\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02!:\x01*\x1a\x1c/v1/platform/module-envelopeB\xba\x01\n" + + "%saas.platform.module_envelope_written\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02!:\x01*\x1a\x1c/v1/platform/module-envelope\x12\xcf\x01\n" + + "\x1bResolveModuleTenantIdentity\x124.saas.accounts.v1.ResolveModuleTenantIdentityRequest\x1a5.saas.accounts.v1.ResolveModuleTenantIdentityResponse\"C\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02%\x12#/v1/platform/module-tenant-identityB\xba\x01\n" + "\x14com.saas.accounts.v1B\x12PlatformAdminProtoP\x01Z,accounts/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" var ( @@ -4061,100 +4412,104 @@ func file_saas_accounts_v1_platform_admin_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_platform_admin_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 55) +var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 59) var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ - (CatalogueEntryKind)(0), // 0: saas.accounts.v1.CatalogueEntryKind - (CatalogueGapReason)(0), // 1: saas.accounts.v1.CatalogueGapReason - (CatalogueObservedVerdict)(0), // 2: saas.accounts.v1.CatalogueObservedVerdict - (*SearchUsersRequest)(nil), // 3: saas.accounts.v1.SearchUsersRequest - (*SearchUsersResponse)(nil), // 4: saas.accounts.v1.SearchUsersResponse - (*ListAllOrganizationsRequest)(nil), // 5: saas.accounts.v1.ListAllOrganizationsRequest - (*PlatformOrganization)(nil), // 6: saas.accounts.v1.PlatformOrganization - (*ListAllOrganizationsResponse)(nil), // 7: saas.accounts.v1.ListAllOrganizationsResponse - (*GetOrganizationRosterRequest)(nil), // 8: saas.accounts.v1.GetOrganizationRosterRequest - (*GetOrganizationRosterResponse)(nil), // 9: saas.accounts.v1.GetOrganizationRosterResponse - (*SuspendUserRequest)(nil), // 10: saas.accounts.v1.SuspendUserRequest - (*UnsuspendUserRequest)(nil), // 11: saas.accounts.v1.UnsuspendUserRequest - (*ImpersonateUserRequest)(nil), // 12: saas.accounts.v1.ImpersonateUserRequest - (*ImpersonateUserResponse)(nil), // 13: saas.accounts.v1.ImpersonateUserResponse - (*StopImpersonationRequest)(nil), // 14: saas.accounts.v1.StopImpersonationRequest - (*StopImpersonationResponse)(nil), // 15: saas.accounts.v1.StopImpersonationResponse - (*ListActiveSessionsRequest)(nil), // 16: saas.accounts.v1.ListActiveSessionsRequest - (*SessionInfo)(nil), // 17: saas.accounts.v1.SessionInfo - (*ListActiveSessionsResponse)(nil), // 18: saas.accounts.v1.ListActiveSessionsResponse - (*RevokeSessionRequest)(nil), // 19: saas.accounts.v1.RevokeSessionRequest - (*GetOrgEntitlementsRequest)(nil), // 20: saas.accounts.v1.GetOrgEntitlementsRequest - (*GetOrgEntitlementsResponse)(nil), // 21: saas.accounts.v1.GetOrgEntitlementsResponse - (*EntitlementInfo)(nil), // 22: saas.accounts.v1.EntitlementInfo - (*OverrideEntitlementRequest)(nil), // 23: saas.accounts.v1.OverrideEntitlementRequest - (*OverrideEntitlementResponse)(nil), // 24: saas.accounts.v1.OverrideEntitlementResponse - (*GrantPlatformRoleRequest)(nil), // 25: saas.accounts.v1.GrantPlatformRoleRequest - (*RevokePlatformRoleRequest)(nil), // 26: saas.accounts.v1.RevokePlatformRoleRequest - (*ListPlatformAdminsRequest)(nil), // 27: saas.accounts.v1.ListPlatformAdminsRequest - (*PlatformAdminEntry)(nil), // 28: saas.accounts.v1.PlatformAdminEntry - (*ListPlatformAdminsResponse)(nil), // 29: saas.accounts.v1.ListPlatformAdminsResponse - (*ListFeatureFlagsRequest)(nil), // 30: saas.accounts.v1.ListFeatureFlagsRequest - (*FeatureFlagEntry)(nil), // 31: saas.accounts.v1.FeatureFlagEntry - (*ListFeatureFlagsResponse)(nil), // 32: saas.accounts.v1.ListFeatureFlagsResponse - (*UpsertFeatureFlagRequest)(nil), // 33: saas.accounts.v1.UpsertFeatureFlagRequest - (*UpsertFeatureFlagResponse)(nil), // 34: saas.accounts.v1.UpsertFeatureFlagResponse - (*CatalogueGap)(nil), // 35: saas.accounts.v1.CatalogueGap - (*CatalogueRelease)(nil), // 36: saas.accounts.v1.CatalogueRelease - (*CatalogueDesired)(nil), // 37: saas.accounts.v1.CatalogueDesired - (*CatalogueAuthorization)(nil), // 38: saas.accounts.v1.CatalogueAuthorization - (*CatalogueNotAuthorized)(nil), // 39: saas.accounts.v1.CatalogueNotAuthorized - (*CatalogueAuthorized)(nil), // 40: saas.accounts.v1.CatalogueAuthorized - (*CatalogueApplied)(nil), // 41: saas.accounts.v1.CatalogueApplied - (*CatalogueObservedExecution)(nil), // 42: saas.accounts.v1.CatalogueObservedExecution - (*CatalogueObserved)(nil), // 43: saas.accounts.v1.CatalogueObserved - (*CatalogueWithdrawing)(nil), // 44: saas.accounts.v1.CatalogueWithdrawing - (*CatalogueRetired)(nil), // 45: saas.accounts.v1.CatalogueRetired - (*CatalogueInstallation)(nil), // 46: saas.accounts.v1.CatalogueInstallation - (*CatalogueEntry)(nil), // 47: saas.accounts.v1.CatalogueEntry - (*ListPlatformCatalogueRequest)(nil), // 48: saas.accounts.v1.ListPlatformCatalogueRequest - (*ListPlatformCatalogueResponse)(nil), // 49: saas.accounts.v1.ListPlatformCatalogueResponse - (*ModuleEnvelope)(nil), // 50: saas.accounts.v1.ModuleEnvelope - (*GetModuleEnvelopeRequest)(nil), // 51: saas.accounts.v1.GetModuleEnvelopeRequest - (*GetModuleEnvelopeResponse)(nil), // 52: saas.accounts.v1.GetModuleEnvelopeResponse - (*PutModuleEnvelopeRequest)(nil), // 53: saas.accounts.v1.PutModuleEnvelopeRequest - (*PutModuleEnvelopeResponse)(nil), // 54: saas.accounts.v1.PutModuleEnvelopeResponse - nil, // 55: saas.accounts.v1.SessionInfo.DeviceInfoEntry - nil, // 56: saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry - nil, // 57: saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry - (*User)(nil), // 58: saas.accounts.v1.User - (*Organization)(nil), // 59: saas.accounts.v1.Organization - (*OrgMembership)(nil), // 60: saas.accounts.v1.OrgMembership - (*timestamppb.Timestamp)(nil), // 61: google.protobuf.Timestamp - (*Installation)(nil), // 62: saas.accounts.v1.Installation - (*CollectionReadGrant)(nil), // 63: saas.accounts.v1.CollectionReadGrant - (*SolutionRegistration)(nil), // 64: saas.accounts.v1.SolutionRegistration - (*v1.GetJobOperationsRequest)(nil), // 65: saas.jobs.v1.GetJobOperationsRequest - (*v1.ListJobsRequest)(nil), // 66: saas.jobs.v1.ListJobsRequest - (*v1.GetJobRequest)(nil), // 67: saas.jobs.v1.GetJobRequest - (*v1.ReplayJobRequest)(nil), // 68: saas.jobs.v1.ReplayJobRequest - (*v11.GetEventOperationsRequest)(nil), // 69: saas.events.v1.GetEventOperationsRequest - (*v11.ListEventSubscriptionsRequest)(nil), // 70: saas.events.v1.ListEventSubscriptionsRequest - (*emptypb.Empty)(nil), // 71: google.protobuf.Empty - (*v1.GetJobOperationsResponse)(nil), // 72: saas.jobs.v1.GetJobOperationsResponse - (*v1.ListJobsResponse)(nil), // 73: saas.jobs.v1.ListJobsResponse - (*v1.GetJobResponse)(nil), // 74: saas.jobs.v1.GetJobResponse - (*v1.ReplayJobResponse)(nil), // 75: saas.jobs.v1.ReplayJobResponse - (*v11.GetEventOperationsResponse)(nil), // 76: saas.events.v1.GetEventOperationsResponse - (*v11.ListEventSubscriptionsResponse)(nil), // 77: saas.events.v1.ListEventSubscriptionsResponse + (CatalogueEntryKind)(0), // 0: saas.accounts.v1.CatalogueEntryKind + (CatalogueGapReason)(0), // 1: saas.accounts.v1.CatalogueGapReason + (CatalogueObservedVerdict)(0), // 2: saas.accounts.v1.CatalogueObservedVerdict + (*SearchUsersRequest)(nil), // 3: saas.accounts.v1.SearchUsersRequest + (*SearchUsersResponse)(nil), // 4: saas.accounts.v1.SearchUsersResponse + (*ListAllOrganizationsRequest)(nil), // 5: saas.accounts.v1.ListAllOrganizationsRequest + (*PlatformOrganization)(nil), // 6: saas.accounts.v1.PlatformOrganization + (*ListAllOrganizationsResponse)(nil), // 7: saas.accounts.v1.ListAllOrganizationsResponse + (*GetOrganizationRosterRequest)(nil), // 8: saas.accounts.v1.GetOrganizationRosterRequest + (*GetOrganizationRosterResponse)(nil), // 9: saas.accounts.v1.GetOrganizationRosterResponse + (*SuspendUserRequest)(nil), // 10: saas.accounts.v1.SuspendUserRequest + (*UnsuspendUserRequest)(nil), // 11: saas.accounts.v1.UnsuspendUserRequest + (*ImpersonateUserRequest)(nil), // 12: saas.accounts.v1.ImpersonateUserRequest + (*ImpersonateUserResponse)(nil), // 13: saas.accounts.v1.ImpersonateUserResponse + (*StopImpersonationRequest)(nil), // 14: saas.accounts.v1.StopImpersonationRequest + (*StopImpersonationResponse)(nil), // 15: saas.accounts.v1.StopImpersonationResponse + (*ListActiveSessionsRequest)(nil), // 16: saas.accounts.v1.ListActiveSessionsRequest + (*SessionInfo)(nil), // 17: saas.accounts.v1.SessionInfo + (*ListActiveSessionsResponse)(nil), // 18: saas.accounts.v1.ListActiveSessionsResponse + (*RevokeSessionRequest)(nil), // 19: saas.accounts.v1.RevokeSessionRequest + (*GetOrgEntitlementsRequest)(nil), // 20: saas.accounts.v1.GetOrgEntitlementsRequest + (*GetOrgEntitlementsResponse)(nil), // 21: saas.accounts.v1.GetOrgEntitlementsResponse + (*EntitlementInfo)(nil), // 22: saas.accounts.v1.EntitlementInfo + (*OverrideEntitlementRequest)(nil), // 23: saas.accounts.v1.OverrideEntitlementRequest + (*OverrideEntitlementResponse)(nil), // 24: saas.accounts.v1.OverrideEntitlementResponse + (*GrantPlatformRoleRequest)(nil), // 25: saas.accounts.v1.GrantPlatformRoleRequest + (*RevokePlatformRoleRequest)(nil), // 26: saas.accounts.v1.RevokePlatformRoleRequest + (*ListPlatformAdminsRequest)(nil), // 27: saas.accounts.v1.ListPlatformAdminsRequest + (*PlatformAdminEntry)(nil), // 28: saas.accounts.v1.PlatformAdminEntry + (*ListPlatformAdminsResponse)(nil), // 29: saas.accounts.v1.ListPlatformAdminsResponse + (*ListFeatureFlagsRequest)(nil), // 30: saas.accounts.v1.ListFeatureFlagsRequest + (*FeatureFlagEntry)(nil), // 31: saas.accounts.v1.FeatureFlagEntry + (*ListFeatureFlagsResponse)(nil), // 32: saas.accounts.v1.ListFeatureFlagsResponse + (*UpsertFeatureFlagRequest)(nil), // 33: saas.accounts.v1.UpsertFeatureFlagRequest + (*UpsertFeatureFlagResponse)(nil), // 34: saas.accounts.v1.UpsertFeatureFlagResponse + (*CatalogueGap)(nil), // 35: saas.accounts.v1.CatalogueGap + (*CatalogueRelease)(nil), // 36: saas.accounts.v1.CatalogueRelease + (*CatalogueDesired)(nil), // 37: saas.accounts.v1.CatalogueDesired + (*CatalogueAuthorization)(nil), // 38: saas.accounts.v1.CatalogueAuthorization + (*CatalogueNotAuthorized)(nil), // 39: saas.accounts.v1.CatalogueNotAuthorized + (*CatalogueAuthorized)(nil), // 40: saas.accounts.v1.CatalogueAuthorized + (*CatalogueApplied)(nil), // 41: saas.accounts.v1.CatalogueApplied + (*CatalogueObservedExecution)(nil), // 42: saas.accounts.v1.CatalogueObservedExecution + (*CatalogueObserved)(nil), // 43: saas.accounts.v1.CatalogueObserved + (*CatalogueWithdrawing)(nil), // 44: saas.accounts.v1.CatalogueWithdrawing + (*CatalogueRetired)(nil), // 45: saas.accounts.v1.CatalogueRetired + (*CatalogueInstallation)(nil), // 46: saas.accounts.v1.CatalogueInstallation + (*CatalogueEntry)(nil), // 47: saas.accounts.v1.CatalogueEntry + (*ListPlatformCatalogueRequest)(nil), // 48: saas.accounts.v1.ListPlatformCatalogueRequest + (*ListPlatformCatalogueResponse)(nil), // 49: saas.accounts.v1.ListPlatformCatalogueResponse + (*ModuleEnvelope)(nil), // 50: saas.accounts.v1.ModuleEnvelope + (*GetModuleEnvelopeRequest)(nil), // 51: saas.accounts.v1.GetModuleEnvelopeRequest + (*GetModuleEnvelopeResponse)(nil), // 52: saas.accounts.v1.GetModuleEnvelopeResponse + (*PutModuleEnvelopeRequest)(nil), // 53: saas.accounts.v1.PutModuleEnvelopeRequest + (*PutModuleEnvelopeResponse)(nil), // 54: saas.accounts.v1.PutModuleEnvelopeResponse + (*ResolveModuleTenantIdentityRequest)(nil), // 55: saas.accounts.v1.ResolveModuleTenantIdentityRequest + (*ModuleTenantIdentity)(nil), // 56: saas.accounts.v1.ModuleTenantIdentity + (*ModuleTenantIdentityMint)(nil), // 57: saas.accounts.v1.ModuleTenantIdentityMint + (*ResolveModuleTenantIdentityResponse)(nil), // 58: saas.accounts.v1.ResolveModuleTenantIdentityResponse + nil, // 59: saas.accounts.v1.SessionInfo.DeviceInfoEntry + nil, // 60: saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry + nil, // 61: saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry + (*User)(nil), // 62: saas.accounts.v1.User + (*Organization)(nil), // 63: saas.accounts.v1.Organization + (*OrgMembership)(nil), // 64: saas.accounts.v1.OrgMembership + (*timestamppb.Timestamp)(nil), // 65: google.protobuf.Timestamp + (*Installation)(nil), // 66: saas.accounts.v1.Installation + (*CollectionReadGrant)(nil), // 67: saas.accounts.v1.CollectionReadGrant + (*SolutionRegistration)(nil), // 68: saas.accounts.v1.SolutionRegistration + (*v1.GetJobOperationsRequest)(nil), // 69: saas.jobs.v1.GetJobOperationsRequest + (*v1.ListJobsRequest)(nil), // 70: saas.jobs.v1.ListJobsRequest + (*v1.GetJobRequest)(nil), // 71: saas.jobs.v1.GetJobRequest + (*v1.ReplayJobRequest)(nil), // 72: saas.jobs.v1.ReplayJobRequest + (*v11.GetEventOperationsRequest)(nil), // 73: saas.events.v1.GetEventOperationsRequest + (*v11.ListEventSubscriptionsRequest)(nil), // 74: saas.events.v1.ListEventSubscriptionsRequest + (*emptypb.Empty)(nil), // 75: google.protobuf.Empty + (*v1.GetJobOperationsResponse)(nil), // 76: saas.jobs.v1.GetJobOperationsResponse + (*v1.ListJobsResponse)(nil), // 77: saas.jobs.v1.ListJobsResponse + (*v1.GetJobResponse)(nil), // 78: saas.jobs.v1.GetJobResponse + (*v1.ReplayJobResponse)(nil), // 79: saas.jobs.v1.ReplayJobResponse + (*v11.GetEventOperationsResponse)(nil), // 80: saas.events.v1.GetEventOperationsResponse + (*v11.ListEventSubscriptionsResponse)(nil), // 81: saas.events.v1.ListEventSubscriptionsResponse } var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ - 58, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User - 59, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization + 62, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User + 63, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization 6, // 2: saas.accounts.v1.ListAllOrganizationsResponse.organizations:type_name -> saas.accounts.v1.PlatformOrganization - 60, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership - 55, // 4: saas.accounts.v1.SessionInfo.device_info:type_name -> saas.accounts.v1.SessionInfo.DeviceInfoEntry - 61, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp - 61, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp - 61, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp - 61, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp + 64, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership + 59, // 4: saas.accounts.v1.SessionInfo.device_info:type_name -> saas.accounts.v1.SessionInfo.DeviceInfoEntry + 65, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp + 65, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp + 65, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp + 65, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp 17, // 9: saas.accounts.v1.ListActiveSessionsResponse.sessions:type_name -> saas.accounts.v1.SessionInfo 22, // 10: saas.accounts.v1.GetOrgEntitlementsResponse.entitlements:type_name -> saas.accounts.v1.EntitlementInfo - 61, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp + 65, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp 28, // 12: saas.accounts.v1.ListPlatformAdminsResponse.admins:type_name -> saas.accounts.v1.PlatformAdminEntry 31, // 13: saas.accounts.v1.ListFeatureFlagsResponse.flags:type_name -> saas.accounts.v1.FeatureFlagEntry 1, // 14: saas.accounts.v1.CatalogueGap.reason:type_name -> saas.accounts.v1.CatalogueGapReason @@ -4165,9 +4520,9 @@ var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ 39, // 19: saas.accounts.v1.CatalogueAuthorized.not_authorized:type_name -> saas.accounts.v1.CatalogueNotAuthorized 35, // 20: saas.accounts.v1.CatalogueAuthorized.authorization_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 21: saas.accounts.v1.CatalogueApplied.applied_revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 56, // 22: saas.accounts.v1.CatalogueObservedExecution.container_image_digests:type_name -> saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry + 60, // 22: saas.accounts.v1.CatalogueObservedExecution.container_image_digests:type_name -> saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry 35, // 23: saas.accounts.v1.CatalogueObserved.observed_revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 61, // 24: saas.accounts.v1.CatalogueObserved.observed_at:type_name -> google.protobuf.Timestamp + 65, // 24: saas.accounts.v1.CatalogueObserved.observed_at:type_name -> google.protobuf.Timestamp 35, // 25: saas.accounts.v1.CatalogueObserved.observation_freshness_gap:type_name -> saas.accounts.v1.CatalogueGap 42, // 26: saas.accounts.v1.CatalogueObserved.observed_execution:type_name -> saas.accounts.v1.CatalogueObservedExecution 35, // 27: saas.accounts.v1.CatalogueObserved.observed_execution_gap:type_name -> saas.accounts.v1.CatalogueGap @@ -4176,12 +4531,12 @@ var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ 35, // 30: saas.accounts.v1.CatalogueWithdrawing.withdrawal_state_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 31: saas.accounts.v1.CatalogueWithdrawing.credential_revocation_state_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 32: saas.accounts.v1.CatalogueRetired.retirement_state_gap:type_name -> saas.accounts.v1.CatalogueGap - 62, // 33: saas.accounts.v1.CatalogueInstallation.installation:type_name -> saas.accounts.v1.Installation + 66, // 33: saas.accounts.v1.CatalogueInstallation.installation:type_name -> saas.accounts.v1.Installation 36, // 34: saas.accounts.v1.CatalogueInstallation.agent_release:type_name -> saas.accounts.v1.CatalogueRelease 35, // 35: saas.accounts.v1.CatalogueInstallation.agent_release_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 36: saas.accounts.v1.CatalogueInstallation.revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 63, // 37: saas.accounts.v1.CatalogueInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant - 63, // 38: saas.accounts.v1.CatalogueInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 67, // 37: saas.accounts.v1.CatalogueInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 67, // 38: saas.accounts.v1.CatalogueInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant 0, // 39: saas.accounts.v1.CatalogueEntry.kind:type_name -> saas.accounts.v1.CatalogueEntryKind 35, // 40: saas.accounts.v1.CatalogueEntry.publisher_gap:type_name -> saas.accounts.v1.CatalogueGap 37, // 41: saas.accounts.v1.CatalogueEntry.desired:type_name -> saas.accounts.v1.CatalogueDesired @@ -4191,68 +4546,73 @@ var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ 44, // 45: saas.accounts.v1.CatalogueEntry.withdrawing:type_name -> saas.accounts.v1.CatalogueWithdrawing 45, // 46: saas.accounts.v1.CatalogueEntry.retired:type_name -> saas.accounts.v1.CatalogueRetired 35, // 47: saas.accounts.v1.CatalogueEntry.build_size_gap:type_name -> saas.accounts.v1.CatalogueGap - 64, // 48: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration + 68, // 48: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration 46, // 49: saas.accounts.v1.CatalogueEntry.installations:type_name -> saas.accounts.v1.CatalogueInstallation 47, // 50: saas.accounts.v1.ListPlatformCatalogueResponse.entries:type_name -> saas.accounts.v1.CatalogueEntry - 57, // 51: saas.accounts.v1.ListPlatformCatalogueResponse.approved_inventories:type_name -> saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry - 61, // 52: saas.accounts.v1.ModuleEnvelope.written_at:type_name -> google.protobuf.Timestamp + 61, // 51: saas.accounts.v1.ListPlatformCatalogueResponse.approved_inventories:type_name -> saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry + 65, // 52: saas.accounts.v1.ModuleEnvelope.written_at:type_name -> google.protobuf.Timestamp 50, // 53: saas.accounts.v1.GetModuleEnvelopeResponse.envelope:type_name -> saas.accounts.v1.ModuleEnvelope 50, // 54: saas.accounts.v1.PutModuleEnvelopeResponse.envelope:type_name -> saas.accounts.v1.ModuleEnvelope - 3, // 55: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest - 10, // 56: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest - 11, // 57: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest - 12, // 58: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest - 14, // 59: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest - 16, // 60: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest - 19, // 61: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest - 20, // 62: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest - 23, // 63: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest - 25, // 64: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest - 26, // 65: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest - 27, // 66: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest - 5, // 67: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest - 8, // 68: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest - 30, // 69: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest - 33, // 70: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest - 65, // 71: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest - 66, // 72: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest - 67, // 73: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest - 68, // 74: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest - 69, // 75: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest - 70, // 76: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest - 48, // 77: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest - 51, // 78: saas.accounts.v1.PlatformAdminService.GetModuleEnvelope:input_type -> saas.accounts.v1.GetModuleEnvelopeRequest - 53, // 79: saas.accounts.v1.PlatformAdminService.PutModuleEnvelope:input_type -> saas.accounts.v1.PutModuleEnvelopeRequest - 4, // 80: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse - 71, // 81: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty - 71, // 82: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty - 13, // 83: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse - 15, // 84: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse - 18, // 85: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse - 71, // 86: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty - 21, // 87: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse - 24, // 88: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse - 71, // 89: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty - 71, // 90: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty - 29, // 91: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse - 7, // 92: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse - 9, // 93: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse - 32, // 94: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse - 34, // 95: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse - 72, // 96: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse - 73, // 97: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse - 74, // 98: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse - 75, // 99: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse - 76, // 100: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse - 77, // 101: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse - 49, // 102: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse - 52, // 103: saas.accounts.v1.PlatformAdminService.GetModuleEnvelope:output_type -> saas.accounts.v1.GetModuleEnvelopeResponse - 54, // 104: saas.accounts.v1.PlatformAdminService.PutModuleEnvelope:output_type -> saas.accounts.v1.PutModuleEnvelopeResponse - 80, // [80:105] is the sub-list for method output_type - 55, // [55:80] is the sub-list for method input_type - 55, // [55:55] is the sub-list for extension type_name - 55, // [55:55] is the sub-list for extension extendee - 0, // [0:55] is the sub-list for field type_name + 65, // 55: saas.accounts.v1.ModuleTenantIdentityMint.minted_at:type_name -> google.protobuf.Timestamp + 56, // 56: saas.accounts.v1.ResolveModuleTenantIdentityResponse.identity:type_name -> saas.accounts.v1.ModuleTenantIdentity + 57, // 57: saas.accounts.v1.ResolveModuleTenantIdentityResponse.recent:type_name -> saas.accounts.v1.ModuleTenantIdentityMint + 3, // 58: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest + 10, // 59: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest + 11, // 60: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest + 12, // 61: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest + 14, // 62: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest + 16, // 63: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest + 19, // 64: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest + 20, // 65: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest + 23, // 66: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest + 25, // 67: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest + 26, // 68: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest + 27, // 69: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest + 5, // 70: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest + 8, // 71: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest + 30, // 72: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest + 33, // 73: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest + 69, // 74: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest + 70, // 75: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest + 71, // 76: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest + 72, // 77: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest + 73, // 78: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest + 74, // 79: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest + 48, // 80: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest + 51, // 81: saas.accounts.v1.PlatformAdminService.GetModuleEnvelope:input_type -> saas.accounts.v1.GetModuleEnvelopeRequest + 53, // 82: saas.accounts.v1.PlatformAdminService.PutModuleEnvelope:input_type -> saas.accounts.v1.PutModuleEnvelopeRequest + 55, // 83: saas.accounts.v1.PlatformAdminService.ResolveModuleTenantIdentity:input_type -> saas.accounts.v1.ResolveModuleTenantIdentityRequest + 4, // 84: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse + 75, // 85: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty + 75, // 86: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty + 13, // 87: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse + 15, // 88: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse + 18, // 89: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse + 75, // 90: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty + 21, // 91: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse + 24, // 92: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse + 75, // 93: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty + 75, // 94: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty + 29, // 95: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse + 7, // 96: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse + 9, // 97: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse + 32, // 98: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse + 34, // 99: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse + 76, // 100: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse + 77, // 101: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse + 78, // 102: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse + 79, // 103: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse + 80, // 104: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse + 81, // 105: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse + 49, // 106: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse + 52, // 107: saas.accounts.v1.PlatformAdminService.GetModuleEnvelope:output_type -> saas.accounts.v1.GetModuleEnvelopeResponse + 54, // 108: saas.accounts.v1.PlatformAdminService.PutModuleEnvelope:output_type -> saas.accounts.v1.PutModuleEnvelopeResponse + 58, // 109: saas.accounts.v1.PlatformAdminService.ResolveModuleTenantIdentity:output_type -> saas.accounts.v1.ResolveModuleTenantIdentityResponse + 84, // [84:110] is the sub-list for method output_type + 58, // [58:84] is the sub-list for method input_type + 58, // [58:58] is the sub-list for extension type_name + 58, // [58:58] is the sub-list for extension extendee + 0, // [0:58] is the sub-list for field type_name } func init() { file_saas_accounts_v1_platform_admin_proto_init() } @@ -4313,7 +4673,7 @@ func file_saas_accounts_v1_platform_admin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_platform_admin_proto_rawDesc), len(file_saas_accounts_v1_platform_admin_proto_rawDesc)), NumEnums: 3, - NumMessages: 55, + NumMessages: 59, NumExtensions: 0, NumServices: 1, }, diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.gw.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.gw.go index d2280caa9..81063e18f 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.gw.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.gw.go @@ -838,6 +838,41 @@ func local_request_PlatformAdminService_PutModuleEnvelope_0(ctx context.Context, return msg, metadata, err } +var filter_PlatformAdminService_ResolveModuleTenantIdentity_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_PlatformAdminService_ResolveModuleTenantIdentity_0(ctx context.Context, marshaler runtime.Marshaler, client PlatformAdminServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ResolveModuleTenantIdentityRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_PlatformAdminService_ResolveModuleTenantIdentity_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ResolveModuleTenantIdentity(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_PlatformAdminService_ResolveModuleTenantIdentity_0(ctx context.Context, marshaler runtime.Marshaler, server PlatformAdminServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ResolveModuleTenantIdentityRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_PlatformAdminService_ResolveModuleTenantIdentity_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ResolveModuleTenantIdentity(ctx, &protoReq) + return msg, metadata, err +} + // RegisterPlatformAdminServiceHandlerServer registers the http handlers for service PlatformAdminService to "mux". // UnaryRPC :call PlatformAdminServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -1304,6 +1339,26 @@ func RegisterPlatformAdminServiceHandlerServer(ctx context.Context, mux *runtime } forward_PlatformAdminService_PutModuleEnvelope_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_PlatformAdminService_ResolveModuleTenantIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", runtime.WithHTTPPathPattern("/v1/platform/module-tenant-identity")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_PlatformAdminService_ResolveModuleTenantIdentity_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PlatformAdminService_ResolveModuleTenantIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -1735,57 +1790,76 @@ func RegisterPlatformAdminServiceHandlerClient(ctx context.Context, mux *runtime } forward_PlatformAdminService_PutModuleEnvelope_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_PlatformAdminService_ResolveModuleTenantIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", runtime.WithHTTPPathPattern("/v1/platform/module-tenant-identity")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_PlatformAdminService_ResolveModuleTenantIdentity_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PlatformAdminService_ResolveModuleTenantIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } var ( - pattern_PlatformAdminService_SearchUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "users"}, "")) - pattern_PlatformAdminService_SuspendUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "users", "user_id"}, "suspend")) - pattern_PlatformAdminService_UnsuspendUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "users", "user_id"}, "unsuspend")) - pattern_PlatformAdminService_ImpersonateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "users", "user_id"}, "impersonate")) - pattern_PlatformAdminService_StopImpersonation_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "impersonation"}, "stop")) - pattern_PlatformAdminService_ListActiveSessions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "sessions"}, "")) - pattern_PlatformAdminService_RevokeSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "sessions", "session_id"}, "")) - pattern_PlatformAdminService_GetOrgEntitlements_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"v1", "platform", "organizations", "org_id", "entitlements"}, "")) - pattern_PlatformAdminService_OverrideEntitlement_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"v1", "platform", "organizations", "org_id", "entitlements"}, "")) - pattern_PlatformAdminService_GrantPlatformRole_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "admins"}, "")) - pattern_PlatformAdminService_RevokePlatformRole_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "admins", "user_id"}, "")) - pattern_PlatformAdminService_ListPlatformAdmins_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "admins"}, "")) - pattern_PlatformAdminService_ListFeatureFlags_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "feature-flags"}, "")) - pattern_PlatformAdminService_UpsertFeatureFlag_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "feature-flags", "name"}, "")) - pattern_PlatformAdminService_GetJobOperations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "platform", "jobs", "operations"}, "")) - pattern_PlatformAdminService_ListJobs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "jobs"}, "")) - pattern_PlatformAdminService_GetJob_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "jobs", "job_id"}, "")) - pattern_PlatformAdminService_ReplayJob_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "jobs", "source_job_id"}, "replay")) - pattern_PlatformAdminService_GetEventOperations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "platform", "events", "operations"}, "")) - pattern_PlatformAdminService_ListEventSubscriptions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "platform", "events", "subscriptions"}, "")) - pattern_PlatformAdminService_ListPlatformCatalogue_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "catalogue"}, "")) - pattern_PlatformAdminService_GetModuleEnvelope_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "module-envelope"}, "")) - pattern_PlatformAdminService_PutModuleEnvelope_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "module-envelope"}, "")) + pattern_PlatformAdminService_SearchUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "users"}, "")) + pattern_PlatformAdminService_SuspendUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "users", "user_id"}, "suspend")) + pattern_PlatformAdminService_UnsuspendUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "users", "user_id"}, "unsuspend")) + pattern_PlatformAdminService_ImpersonateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "users", "user_id"}, "impersonate")) + pattern_PlatformAdminService_StopImpersonation_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "impersonation"}, "stop")) + pattern_PlatformAdminService_ListActiveSessions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "sessions"}, "")) + pattern_PlatformAdminService_RevokeSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "sessions", "session_id"}, "")) + pattern_PlatformAdminService_GetOrgEntitlements_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"v1", "platform", "organizations", "org_id", "entitlements"}, "")) + pattern_PlatformAdminService_OverrideEntitlement_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"v1", "platform", "organizations", "org_id", "entitlements"}, "")) + pattern_PlatformAdminService_GrantPlatformRole_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "admins"}, "")) + pattern_PlatformAdminService_RevokePlatformRole_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "admins", "user_id"}, "")) + pattern_PlatformAdminService_ListPlatformAdmins_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "admins"}, "")) + pattern_PlatformAdminService_ListFeatureFlags_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "feature-flags"}, "")) + pattern_PlatformAdminService_UpsertFeatureFlag_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "feature-flags", "name"}, "")) + pattern_PlatformAdminService_GetJobOperations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "platform", "jobs", "operations"}, "")) + pattern_PlatformAdminService_ListJobs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "jobs"}, "")) + pattern_PlatformAdminService_GetJob_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "jobs", "job_id"}, "")) + pattern_PlatformAdminService_ReplayJob_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "jobs", "source_job_id"}, "replay")) + pattern_PlatformAdminService_GetEventOperations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "platform", "events", "operations"}, "")) + pattern_PlatformAdminService_ListEventSubscriptions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "platform", "events", "subscriptions"}, "")) + pattern_PlatformAdminService_ListPlatformCatalogue_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "catalogue"}, "")) + pattern_PlatformAdminService_GetModuleEnvelope_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "module-envelope"}, "")) + pattern_PlatformAdminService_PutModuleEnvelope_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "module-envelope"}, "")) + pattern_PlatformAdminService_ResolveModuleTenantIdentity_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "module-tenant-identity"}, "")) ) var ( - forward_PlatformAdminService_SearchUsers_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_SuspendUser_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_UnsuspendUser_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_ImpersonateUser_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_StopImpersonation_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_ListActiveSessions_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_RevokeSession_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_GetOrgEntitlements_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_OverrideEntitlement_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_GrantPlatformRole_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_RevokePlatformRole_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_ListPlatformAdmins_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_ListFeatureFlags_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_UpsertFeatureFlag_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_GetJobOperations_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_ListJobs_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_GetJob_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_ReplayJob_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_GetEventOperations_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_ListEventSubscriptions_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_ListPlatformCatalogue_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_GetModuleEnvelope_0 = runtime.ForwardResponseMessage - forward_PlatformAdminService_PutModuleEnvelope_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_SearchUsers_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_SuspendUser_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_UnsuspendUser_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ImpersonateUser_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_StopImpersonation_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ListActiveSessions_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_RevokeSession_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_GetOrgEntitlements_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_OverrideEntitlement_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_GrantPlatformRole_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_RevokePlatformRole_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ListPlatformAdmins_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ListFeatureFlags_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_UpsertFeatureFlag_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_GetJobOperations_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ListJobs_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_GetJob_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ReplayJob_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_GetEventOperations_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ListEventSubscriptions_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ListPlatformCatalogue_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_GetModuleEnvelope_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_PutModuleEnvelope_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ResolveModuleTenantIdentity_0 = runtime.ForwardResponseMessage ) diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin_grpc.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin_grpc.pb.go index 3b68a7de0..a377abeb9 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin_grpc.pb.go @@ -23,31 +23,32 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - PlatformAdminService_SearchUsers_FullMethodName = "/saas.accounts.v1.PlatformAdminService/SearchUsers" - PlatformAdminService_SuspendUser_FullMethodName = "/saas.accounts.v1.PlatformAdminService/SuspendUser" - PlatformAdminService_UnsuspendUser_FullMethodName = "/saas.accounts.v1.PlatformAdminService/UnsuspendUser" - PlatformAdminService_ImpersonateUser_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ImpersonateUser" - PlatformAdminService_StopImpersonation_FullMethodName = "/saas.accounts.v1.PlatformAdminService/StopImpersonation" - PlatformAdminService_ListActiveSessions_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListActiveSessions" - PlatformAdminService_RevokeSession_FullMethodName = "/saas.accounts.v1.PlatformAdminService/RevokeSession" - PlatformAdminService_GetOrgEntitlements_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetOrgEntitlements" - PlatformAdminService_OverrideEntitlement_FullMethodName = "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement" - PlatformAdminService_GrantPlatformRole_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GrantPlatformRole" - PlatformAdminService_RevokePlatformRole_FullMethodName = "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole" - PlatformAdminService_ListPlatformAdmins_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins" - PlatformAdminService_ListAllOrganizations_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListAllOrganizations" - PlatformAdminService_GetOrganizationRoster_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetOrganizationRoster" - PlatformAdminService_ListFeatureFlags_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags" - PlatformAdminService_UpsertFeatureFlag_FullMethodName = "/saas.accounts.v1.PlatformAdminService/UpsertFeatureFlag" - PlatformAdminService_GetJobOperations_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetJobOperations" - PlatformAdminService_ListJobs_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListJobs" - PlatformAdminService_GetJob_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetJob" - PlatformAdminService_ReplayJob_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ReplayJob" - PlatformAdminService_GetEventOperations_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetEventOperations" - PlatformAdminService_ListEventSubscriptions_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListEventSubscriptions" - PlatformAdminService_ListPlatformCatalogue_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue" - PlatformAdminService_GetModuleEnvelope_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetModuleEnvelope" - PlatformAdminService_PutModuleEnvelope_FullMethodName = "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope" + PlatformAdminService_SearchUsers_FullMethodName = "/saas.accounts.v1.PlatformAdminService/SearchUsers" + PlatformAdminService_SuspendUser_FullMethodName = "/saas.accounts.v1.PlatformAdminService/SuspendUser" + PlatformAdminService_UnsuspendUser_FullMethodName = "/saas.accounts.v1.PlatformAdminService/UnsuspendUser" + PlatformAdminService_ImpersonateUser_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ImpersonateUser" + PlatformAdminService_StopImpersonation_FullMethodName = "/saas.accounts.v1.PlatformAdminService/StopImpersonation" + PlatformAdminService_ListActiveSessions_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListActiveSessions" + PlatformAdminService_RevokeSession_FullMethodName = "/saas.accounts.v1.PlatformAdminService/RevokeSession" + PlatformAdminService_GetOrgEntitlements_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetOrgEntitlements" + PlatformAdminService_OverrideEntitlement_FullMethodName = "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement" + PlatformAdminService_GrantPlatformRole_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GrantPlatformRole" + PlatformAdminService_RevokePlatformRole_FullMethodName = "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole" + PlatformAdminService_ListPlatformAdmins_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins" + PlatformAdminService_ListAllOrganizations_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListAllOrganizations" + PlatformAdminService_GetOrganizationRoster_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetOrganizationRoster" + PlatformAdminService_ListFeatureFlags_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags" + PlatformAdminService_UpsertFeatureFlag_FullMethodName = "/saas.accounts.v1.PlatformAdminService/UpsertFeatureFlag" + PlatformAdminService_GetJobOperations_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetJobOperations" + PlatformAdminService_ListJobs_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListJobs" + PlatformAdminService_GetJob_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetJob" + PlatformAdminService_ReplayJob_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ReplayJob" + PlatformAdminService_GetEventOperations_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetEventOperations" + PlatformAdminService_ListEventSubscriptions_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListEventSubscriptions" + PlatformAdminService_ListPlatformCatalogue_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue" + PlatformAdminService_GetModuleEnvelope_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetModuleEnvelope" + PlatformAdminService_PutModuleEnvelope_FullMethodName = "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope" + PlatformAdminService_ResolveModuleTenantIdentity_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity" ) // PlatformAdminServiceClient is the client API for PlatformAdminService service. @@ -114,6 +115,10 @@ type PlatformAdminServiceClient interface { // effect at the next check with no restart: the module registry is derived // again from it, so a narrowing removes what the new ceiling no longer allows. PutModuleEnvelope(ctx context.Context, in *PutModuleEnvelopeRequest, opts ...grpc.CallOption) (*PutModuleEnvelopeResponse, error) + // ResolveModuleTenantIdentity answers what a per-tenant module operation + // context would assert for one module, one of its bindings and one + // organization — or why it would be refused. It issues no capability. + ResolveModuleTenantIdentity(ctx context.Context, in *ResolveModuleTenantIdentityRequest, opts ...grpc.CallOption) (*ResolveModuleTenantIdentityResponse, error) } type platformAdminServiceClient struct { @@ -375,6 +380,16 @@ func (c *platformAdminServiceClient) PutModuleEnvelope(ctx context.Context, in * return out, nil } +func (c *platformAdminServiceClient) ResolveModuleTenantIdentity(ctx context.Context, in *ResolveModuleTenantIdentityRequest, opts ...grpc.CallOption) (*ResolveModuleTenantIdentityResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResolveModuleTenantIdentityResponse) + err := c.cc.Invoke(ctx, PlatformAdminService_ResolveModuleTenantIdentity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // PlatformAdminServiceServer is the server API for PlatformAdminService service. // All implementations must embed UnimplementedPlatformAdminServiceServer // for forward compatibility. @@ -439,6 +454,10 @@ type PlatformAdminServiceServer interface { // effect at the next check with no restart: the module registry is derived // again from it, so a narrowing removes what the new ceiling no longer allows. PutModuleEnvelope(context.Context, *PutModuleEnvelopeRequest) (*PutModuleEnvelopeResponse, error) + // ResolveModuleTenantIdentity answers what a per-tenant module operation + // context would assert for one module, one of its bindings and one + // organization — or why it would be refused. It issues no capability. + ResolveModuleTenantIdentity(context.Context, *ResolveModuleTenantIdentityRequest) (*ResolveModuleTenantIdentityResponse, error) mustEmbedUnimplementedPlatformAdminServiceServer() } @@ -524,6 +543,9 @@ func (UnimplementedPlatformAdminServiceServer) GetModuleEnvelope(context.Context func (UnimplementedPlatformAdminServiceServer) PutModuleEnvelope(context.Context, *PutModuleEnvelopeRequest) (*PutModuleEnvelopeResponse, error) { return nil, status.Error(codes.Unimplemented, "method PutModuleEnvelope not implemented") } +func (UnimplementedPlatformAdminServiceServer) ResolveModuleTenantIdentity(context.Context, *ResolveModuleTenantIdentityRequest) (*ResolveModuleTenantIdentityResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ResolveModuleTenantIdentity not implemented") +} func (UnimplementedPlatformAdminServiceServer) mustEmbedUnimplementedPlatformAdminServiceServer() {} func (UnimplementedPlatformAdminServiceServer) testEmbeddedByValue() {} @@ -995,6 +1017,24 @@ func _PlatformAdminService_PutModuleEnvelope_Handler(srv interface{}, ctx contex return interceptor(ctx, in, info, handler) } +func _PlatformAdminService_ResolveModuleTenantIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ResolveModuleTenantIdentityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlatformAdminServiceServer).ResolveModuleTenantIdentity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PlatformAdminService_ResolveModuleTenantIdentity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlatformAdminServiceServer).ResolveModuleTenantIdentity(ctx, req.(*ResolveModuleTenantIdentityRequest)) + } + return interceptor(ctx, in, info, handler) +} + // PlatformAdminService_ServiceDesc is the grpc.ServiceDesc for PlatformAdminService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1102,6 +1142,10 @@ var PlatformAdminService_ServiceDesc = grpc.ServiceDesc{ MethodName: "PutModuleEnvelope", Handler: _PlatformAdminService_PutModuleEnvelope_Handler, }, + { + MethodName: "ResolveModuleTenantIdentity", + Handler: _PlatformAdminService_ResolveModuleTenantIdentity_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "saas/accounts/v1/platform_admin.proto", diff --git a/module/services/accounts/generated/api-contract-surfaces.json b/module/services/accounts/generated/api-contract-surfaces.json index 749e0093d..4749d1698 100644 --- a/module/services/accounts/generated/api-contract-surfaces.json +++ b/module/services/accounts/generated/api-contract-surfaces.json @@ -386,6 +386,7 @@ "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope", "/saas.accounts.v1.PlatformAdminService/ReplayJob", + "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", "/saas.accounts.v1.PlatformAdminService/RevokeSession", "/saas.accounts.v1.PlatformAdminService/SearchUsers", diff --git a/module/services/accounts/generated/authz-methods.json b/module/services/accounts/generated/authz-methods.json index 47b4db1b7..0990fb691 100644 --- a/module/services/accounts/generated/authz-methods.json +++ b/module/services/accounts/generated/authz-methods.json @@ -8222,6 +8222,43 @@ "policy_sha256": "ca332de30ec2ccedf0bc703de994eb50cb7f2055c2574c330b0cb421134caf25", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", + "full_service": "saas.accounts.v1.PlatformAdminService", + "method": "ResolveModuleTenantIdentity", + "input_type": "saas.accounts.v1.ResolveModuleTenantIdentityRequest", + "output_type": "saas.accounts.v1.ResolveModuleTenantIdentityResponse", + "client_streaming": false, + "server_streaming": false, + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_SENSITIVE", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "platform_admin", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "source_proto": "saas/accounts/v1/platform_admin.proto", + "policy_sha256": "e3ae6ee3f1b70dbabb131e3a486c403859c0f3540c61dca8132317bd1be9d7b3", + "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" + }, { "procedure": "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", "full_service": "saas.accounts.v1.PlatformAdminService", diff --git a/module/services/accounts/generated/gateway-routes.json b/module/services/accounts/generated/gateway-routes.json index 70a35eb51..6c28cf34c 100644 --- a/module/services/accounts/generated/gateway-routes.json +++ b/module/services/accounts/generated/gateway-routes.json @@ -2545,6 +2545,22 @@ "rewrite_path": "/saas.accounts.v1.PlatformAdminService/ReplayJob", "remove_after": "2026-10-11" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/customers.PlatformAdminService/ResolveModuleTenantIdentity", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_COMPATIBILITY_ALIAS", + "rewrite_path": "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", + "remove_after": "2026-10-11" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", @@ -5985,6 +6001,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", @@ -7873,6 +7905,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_REST", + "method": "GET", + "path": "/v1/platform/module-tenant-identity", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "rest", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_REST", "method": "GET", diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 72aaa430c..37797034a 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -4032,6 +4032,51 @@ ] } }, + "/v1/platform/module-tenant-identity": { + "get": { + "summary": "ResolveModuleTenantIdentity answers what a per-tenant module operation\ncontext would assert for one module, one of its bindings and one\norganization — or why it would be refused. It issues no capability.", + "operationId": "PlatformAdminService_ResolveModuleTenantIdentity", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ResolveModuleTenantIdentityResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "prefix", + "description": "prefix is the module principal's registration prefix and binding the key\nof its operation_audiences entry.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "binding", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "organizationId", + "description": "organization_id is the tenant the capability would act for.", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "PlatformAdminService" + ] + } + }, "/v1/platform/organizations/{orgId}/entitlements": { "get": { "summary": "Entitlements \u0026 billing", @@ -10107,6 +10152,72 @@ }, "description": "ModuleEnvelope is one revision of the module envelope: the authority ceiling a\nplatform super_admin writes, which every module principal on this host is\nderived inside. Revisions are append-only; the current ceiling is the\nhighest." }, + "v1ModuleTenantIdentity": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "principal_id is the module's own service principal, which owns the\ncapability and acts directly — there is no person and no actor hop." + }, + "tenant": { + "type": "string", + "description": "tenant is the organization it would act for, and audience the service the\nbinding declares it may call. A binding whose audience is the module's own\nprefix is one the module admits itself, which is how unattended work is\nadmitted by the module that executes it." + }, + "audience": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "scopes is exactly what the capability would carry, as\n`kind:action[:resource]`: the binding's headless_scopes narrowed by THIS\norganization's installation scope. Never its invoke_scopes, which describe\nauthority exercised on a person's behalf." + }, + "lifetimeSeconds": { + "type": "string", + "format": "int64", + "description": "lifetime_seconds is the fixed lifetime every such capability carries." + }, + "revision": { + "type": "string", + "format": "uint64", + "description": "revision is the authorization revision it would be sealed with: a digest\nof the module's whole effective entry, so any change to the entry stops\nconfirming every capability minted under the old one." + }, + "shared": { + "type": "boolean", + "description": "shared says the principal is unpinned and declares cross_tenant, so it may\nact for more than one organization. A pinned principal reads false and can\nonly ever name the tenant its envelope declares." + } + }, + "description": "ModuleTenantIdentity is what one per-tenant mint would assert. Every field is\nthe host's: nothing here is requestable." + }, + "v1ModuleTenantIdentityMint": { + "type": "object", + "properties": { + "record": { + "type": "string" + }, + "tenant": { + "type": "string" + }, + "binding": { + "type": "string" + }, + "audience": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "mintedAt": { + "type": "string", + "format": "date-time" + } + }, + "description": "ModuleTenantIdentityMint is one capability actually issued, as the audit\ntrail recorded it. There is no other record: a capability lives a minute and\nnothing caches one." + }, "v1Notification": { "type": "object", "properties": { @@ -11036,6 +11147,26 @@ "v1RequestDeletionRequest": { "type": "object" }, + "v1ResolveModuleTenantIdentityResponse": { + "type": "object", + "properties": { + "identity": { + "$ref": "#/definitions/v1ModuleTenantIdentity", + "description": "identity is set when a mint for this organization would be issued; refusal\nsays why it would not. Exactly one is present, and a refusal is a decision\nabout declared authority rather than an outage." + }, + "refusal": { + "type": "string" + }, + "recent": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ModuleTenantIdentityMint" + }, + "description": "recent is the trail of capabilities this module was actually issued for\nthis organization, newest first." + } + } + }, "v1Role": { "type": "object", "properties": { diff --git a/module/services/accounts/generated/rest-surface.json b/module/services/accounts/generated/rest-surface.json index 54a7998bc..2ade0c91f 100644 --- a/module/services/accounts/generated/rest-surface.json +++ b/module/services/accounts/generated/rest-surface.json @@ -997,6 +997,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_REST", + "method": "GET", + "path": "/v1/platform/module-tenant-identity", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "rest", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_REST", "method": "GET", diff --git a/module/services/accounts/generated/service-catalog.json b/module/services/accounts/generated/service-catalog.json index 2108d178e..66bcbe763 100644 --- a/module/services/accounts/generated/service-catalog.json +++ b/module/services/accounts/generated/service-catalog.json @@ -348,6 +348,7 @@ "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope", "/saas.accounts.v1.PlatformAdminService/ReplayJob", + "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", "/saas.accounts.v1.PlatformAdminService/RevokeSession", "/saas.accounts.v1.PlatformAdminService/SearchUsers", @@ -10831,6 +10832,57 @@ "service": "accounts" } }, + { + "procedure": "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", + "service": "PlatformAdminService", + "full_service": "saas.accounts.v1.PlatformAdminService", + "method": "ResolveModuleTenantIdentity", + "input_type": "saas.accounts.v1.ResolveModuleTenantIdentityRequest", + "output_type": "saas.accounts.v1.ResolveModuleTenantIdentityResponse", + "client_streaming": false, + "server_streaming": false, + "deprecated": false, + "source_proto": "saas/accounts/v1/platform_admin.proto", + "protocols": [ + "PROTOCOL_GRPC", + "PROTOCOL_CONNECT", + "PROTOCOL_REST" + ], + "http_bindings": [ + { + "method": "GET", + "path": "/v1/platform/module-tenant-identity", + "body": "", + "response_body": "" + } + ], + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_SENSITIVE", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "platform_admin", + "description": "Read what a per-tenant module operation context would assert for one module, binding and organization, or why it would be refused.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", "service": "PlatformAdminService", diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index 684ac6c2c..ab4551f9b 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -3620,6 +3620,72 @@ }, "type": "object" }, + "v1ModuleTenantIdentity": { + "description": "ModuleTenantIdentity is what one per-tenant mint would assert. Every field is\nthe host's: nothing here is requestable.", + "properties": { + "audience": { + "type": "string" + }, + "lifetimeSeconds": { + "description": "lifetime_seconds is the fixed lifetime every such capability carries.", + "format": "int64", + "type": "string" + }, + "principalId": { + "description": "principal_id is the module's own service principal, which owns the\ncapability and acts directly — there is no person and no actor hop.", + "type": "string" + }, + "revision": { + "description": "revision is the authorization revision it would be sealed with: a digest\nof the module's whole effective entry, so any change to the entry stops\nconfirming every capability minted under the old one.", + "format": "uint64", + "type": "string" + }, + "scopes": { + "description": "scopes is exactly what the capability would carry, as\n`kind:action[:resource]`: the binding's headless_scopes narrowed by THIS\norganization's installation scope. Never its invoke_scopes, which describe\nauthority exercised on a person's behalf.", + "items": { + "type": "string" + }, + "type": "array" + }, + "shared": { + "description": "shared says the principal is unpinned and declares cross_tenant, so it may\nact for more than one organization. A pinned principal reads false and can\nonly ever name the tenant its envelope declares.", + "type": "boolean" + }, + "tenant": { + "description": "tenant is the organization it would act for, and audience the service the\nbinding declares it may call. A binding whose audience is the module's own\nprefix is one the module admits itself, which is how unattended work is\nadmitted by the module that executes it.", + "type": "string" + } + }, + "type": "object" + }, + "v1ModuleTenantIdentityMint": { + "description": "ModuleTenantIdentityMint is one capability actually issued, as the audit\ntrail recorded it. There is no other record: a capability lives a minute and\nnothing caches one.", + "properties": { + "audience": { + "type": "string" + }, + "binding": { + "type": "string" + }, + "mintedAt": { + "format": "date-time", + "type": "string" + }, + "record": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "tenant": { + "type": "string" + } + }, + "type": "object" + }, "v1Notification": { "properties": { "actionUrl": { @@ -4549,6 +4615,26 @@ "v1RequestDeletionRequest": { "type": "object" }, + "v1ResolveModuleTenantIdentityResponse": { + "properties": { + "identity": { + "$ref": "#/definitions/v1ModuleTenantIdentity", + "description": "identity is set when a mint for this organization would be issued; refusal\nsays why it would not. Exactly one is present, and a refusal is a decision\nabout declared authority rather than an outage." + }, + "recent": { + "description": "recent is the trail of capabilities this module was actually issued for\nthis organization, newest first.", + "items": { + "$ref": "#/definitions/v1ModuleTenantIdentityMint", + "type": "object" + }, + "type": "array" + }, + "refusal": { + "type": "string" + } + }, + "type": "object" + }, "v1Role": { "properties": { "builtIn": { @@ -9850,6 +9936,51 @@ ] } }, + "/v1/platform/module-tenant-identity": { + "get": { + "operationId": "PlatformAdminService_ResolveModuleTenantIdentity", + "parameters": [ + { + "description": "prefix is the module principal's registration prefix and binding the key\nof its operation_audiences entry.", + "in": "query", + "name": "prefix", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "binding", + "required": false, + "type": "string" + }, + { + "description": "organization_id is the tenant the capability would act for.", + "in": "query", + "name": "organizationId", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ResolveModuleTenantIdentityResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "summary": "ResolveModuleTenantIdentity answers what a per-tenant module operation\ncontext would assert for one module, one of its bindings and one\norganization — or why it would be refused. It issues no capability.", + "tags": [ + "PlatformAdminService" + ] + } + }, "/v1/platform/organizations/{orgId}/entitlements": { "get": { "operationId": "PlatformAdminService_GetOrgEntitlements", diff --git a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto index 4e955b079..271f1452b 100644 --- a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto +++ b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto @@ -576,6 +576,100 @@ message PutModuleEnvelopeResponse { ModuleEnvelope envelope = 1; } +// --------------------------------------------------------------------------- +// A module identity minted for ONE tenant. +// +// A composed module acting with no person present mints a Work Context for one +// of its installed operation bindings (MintModuleOperationContext) and names +// the organization it is acting FOR. That is what lets a module present itself +// as acting for a tenant rather than as itself: the capability's tenant is the +// organization named, its scopes are that organization's installation narrowed +// to the binding's headless_scopes, it lives a minute, and every issuance is +// recorded. +// +// Which organizations it may name is not the caller's to choose. A principal +// the envelope PINS to a tenant may name that tenant and no other; an unpinned +// principal declaring cross_tenant may name an organization that has a live +// installation consenting to the binding, and nothing else. An organization +// whose installation consents to less gets less, and one that consents to +// nothing is refused — so the bound is per tenant, not per deployment. +// +// This read answers what that mint would assert, for an operator, WITHOUT +// issuing a capability. A browser holds no module identity (the mint requires +// the pod's projected ServiceAccount token, held against the build its +// authority document approves), and a path that handed an operator a module's +// capability would be a second spelling of that identity — so there is +// deliberately no such path, here or anywhere. +// --------------------------------------------------------------------------- + +message ResolveModuleTenantIdentityRequest { + // prefix is the module principal's registration prefix and binding the key + // of its operation_audiences entry. + string prefix = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 63 + pattern: "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" + }]; + string binding = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 128 + }]; + // organization_id is the tenant the capability would act for. + string organization_id = 3 [(buf.validate.field).string.uuid = true]; +} + +// ModuleTenantIdentity is what one per-tenant mint would assert. Every field is +// the host's: nothing here is requestable. +message ModuleTenantIdentity { + // principal_id is the module's own service principal, which owns the + // capability and acts directly — there is no person and no actor hop. + string principal_id = 1; + // tenant is the organization it would act for, and audience the service the + // binding declares it may call. A binding whose audience is the module's own + // prefix is one the module admits itself, which is how unattended work is + // admitted by the module that executes it. + string tenant = 2; + string audience = 3; + // scopes is exactly what the capability would carry, as + // `kind:action[:resource]`: the binding's headless_scopes narrowed by THIS + // organization's installation scope. Never its invoke_scopes, which describe + // authority exercised on a person's behalf. + repeated string scopes = 4; + // lifetime_seconds is the fixed lifetime every such capability carries. + int64 lifetime_seconds = 5; + // revision is the authorization revision it would be sealed with: a digest + // of the module's whole effective entry, so any change to the entry stops + // confirming every capability minted under the old one. + uint64 revision = 6; + // shared says the principal is unpinned and declares cross_tenant, so it may + // act for more than one organization. A pinned principal reads false and can + // only ever name the tenant its envelope declares. + bool shared = 7; +} + +// ModuleTenantIdentityMint is one capability actually issued, as the audit +// trail recorded it. There is no other record: a capability lives a minute and +// nothing caches one. +message ModuleTenantIdentityMint { + string record = 1; + string tenant = 2; + string binding = 3; + string audience = 4; + repeated string scopes = 5; + google.protobuf.Timestamp minted_at = 6; +} + +message ResolveModuleTenantIdentityResponse { + // identity is set when a mint for this organization would be issued; refusal + // says why it would not. Exactly one is present, and a refusal is a decision + // about declared authority rather than an outage. + ModuleTenantIdentity identity = 1; + string refusal = 2; + // recent is the trail of capabilities this module was actually issued for + // this organization, newest first. + repeated ModuleTenantIdentityMint recent = 3; +} + // PlatformAdminService — cross-tenant operations for platform operators. // All RPCs require a platform_role in the JWT claims. service PlatformAdminService { @@ -1041,4 +1135,22 @@ service PlatformAdminService { body: "*" }; } + + // ResolveModuleTenantIdentity answers what a per-tenant module operation + // context would assert for one module, one of its bindings and one + // organization — or why it would be refused. It issues no capability. + rpc ResolveModuleTenantIdentity(ResolveModuleTenantIdentityRequest) returns (ResolveModuleTenantIdentityResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_SENSITIVE + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN + }; + option (google.api.http) = {get: "/v1/platform/module-tenant-identity"}; + } } diff --git a/module/services/auth-gateway/code/authz_catalog_gen.go b/module/services/auth-gateway/code/authz_catalog_gen.go index 45f198f92..15d979260 100644 --- a/module/services/auth-gateway/code/authz_catalog_gen.go +++ b/module/services/auth-gateway/code/authz_catalog_gen.go @@ -210,6 +210,7 @@ var generatedAuthorizationByProcedure = map[string]generatedAuthorizationMetadat "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "3665682c29d02878db8cbfd5fee73da95fb61edf4b3a9329fcb6c13c5decaf18"}, "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "f291952d9b1fbb97dad7ac5752b5f20ebc2bd30ffe47b8859cd70ac7b066f68c"}, "/saas.accounts.v1.PlatformAdminService/ReplayJob": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "ca332de30ec2ccedf0bc703de994eb50cb7f2055c2574c330b0cb421134caf25"}, + "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "e3ae6ee3f1b70dbabb131e3a486c403859c0f3540c61dca8132317bd1be9d7b3"}, "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "5d13d1bec715a08e7cac379958c4e6ad88776a9cac020912bad2c3724e58f514"}, "/saas.accounts.v1.PlatformAdminService/RevokeSession": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "c64db604571a00374e20ee37308bfeaec9e60d025a749aed74df6ddb7c3e24a4"}, "/saas.accounts.v1.PlatformAdminService/SearchUsers": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "e8e73c253ede8c5e55cbebd2c2879342ee7110e147a6f8cbfc9e9150632551c5"}, @@ -397,6 +398,7 @@ var generatedRESTProcedureByRoute = map[string]string{ "POST /v1/platform/organizations/{org_id}/entitlements": "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "PUT /v1/platform/module-envelope": "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope", "POST /v1/platform/jobs/{source_job_id}:replay": "/saas.accounts.v1.PlatformAdminService/ReplayJob", + "GET /v1/platform/module-tenant-identity": "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", "DELETE /v1/platform/admins/{user_id}": "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", "DELETE /v1/platform/sessions/{session_id}": "/saas.accounts.v1.PlatformAdminService/RevokeSession", "GET /v1/platform/users": "/saas.accounts.v1.PlatformAdminService/SearchUsers", diff --git a/module/services/auth-gateway/code/routing_catalog_gen.go b/module/services/auth-gateway/code/routing_catalog_gen.go index dae8fd049..7f107ad15 100644 --- a/module/services/auth-gateway/code/routing_catalog_gen.go +++ b/module/services/auth-gateway/code/routing_catalog_gen.go @@ -165,6 +165,7 @@ func generatedCatalogConnectRoutes() []*RouteEntry { {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/OverrideEntitlement", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", Procedure: "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/PutModuleEnvelope", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope", Procedure: "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/ReplayJob", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/ReplayJob", Procedure: "/saas.accounts.v1.PlatformAdminService/ReplayJob"}, + {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/ResolveModuleTenantIdentity", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", Procedure: "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/RevokePlatformRole", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", Procedure: "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/RevokeSession", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/RevokeSession", Procedure: "/saas.accounts.v1.PlatformAdminService/RevokeSession"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/SearchUsers", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/SearchUsers", Procedure: "/saas.accounts.v1.PlatformAdminService/SearchUsers"}, @@ -380,6 +381,7 @@ func generatedCatalogConnectRoutes() []*RouteEntry { {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/ReplayJob", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/ReplayJob"}, + {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/RevokeSession", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/RevokeSession"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/SearchUsers", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/SearchUsers"}, diff --git a/module/services/auth-gateway/code/routing_rest_catalog_gen.go b/module/services/auth-gateway/code/routing_rest_catalog_gen.go index 734a3759c..9a9cd80fc 100644 --- a/module/services/auth-gateway/code/routing_rest_catalog_gen.go +++ b/module/services/auth-gateway/code/routing_rest_catalog_gen.go @@ -66,6 +66,7 @@ func generatedCatalogRESTRoutes() []*RouteEntry { {Service: "accounts", Method: "GET", Path: "/v1/platform/jobs/operations", Procedure: "/saas.accounts.v1.PlatformAdminService/GetJobOperations"}, {Service: "accounts", Method: "GET", Path: "/v1/platform/jobs/{job_id}", Procedure: "/saas.accounts.v1.PlatformAdminService/GetJob"}, {Service: "accounts", Method: "GET", Path: "/v1/platform/module-envelope", Procedure: "/saas.accounts.v1.PlatformAdminService/GetModuleEnvelope"}, + {Service: "accounts", Method: "GET", Path: "/v1/platform/module-tenant-identity", Procedure: "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity"}, {Service: "accounts", Method: "GET", Path: "/v1/platform/organizations/{org_id}/entitlements", Procedure: "/saas.accounts.v1.PlatformAdminService/GetOrgEntitlements"}, {Service: "accounts", Method: "GET", Path: "/v1/platform/sessions", Procedure: "/saas.accounts.v1.PlatformAdminService/ListActiveSessions"}, {Service: "accounts", Method: "GET", Path: "/v1/platform/users", Procedure: "/saas.accounts.v1.PlatformAdminService/SearchUsers"}, diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 9e8d07cd0..164ed3024 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:0a02fc7e630eb13971649710dd7b3058ab843ee281065b1532b17821c07dac8a", + "digest": "sha256:8ac64d948bd881562508736eeb88fd7031b1b862ed43bfaa58c80090d8afae29", "services": [ { "name": "APIKeyService", @@ -353,6 +353,7 @@ "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope", "/saas.accounts.v1.PlatformAdminService/ReplayJob", + "/saas.accounts.v1.PlatformAdminService/ResolveModuleTenantIdentity", "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", "/saas.accounts.v1.PlatformAdminService/RevokeSession", "/saas.accounts.v1.PlatformAdminService/SearchUsers", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 791a05d33337a12a7d82bf2cde1a48ae200f9c73..7e055997b4caf7907b5efa1ab25f1867a1f86ad1 100644 GIT binary patch delta 34984 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 delta 27337 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 A$^ZZW diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index ac4af2c3d..3f3f90171 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:0a02fc7e630eb13971649710dd7b3058ab843ee281065b1532b17821c07dac8a + contract-digest: sha256:8ac64d948bd881562508736eeb88fd7031b1b862ed43bfaa58c80090d8afae29 services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts index 261e7041d..c7e2e9260 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCJhChlHZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjcKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZUgAiAEBQgsKCV9lbnZlbG9wZSJdChhQdXRNb2R1bGVFbnZlbG9wZVJlcXVlc3QSIgoRZXhwZWN0ZWRfcmV2aXNpb24YASABKANCB7pIBCICKAASHQoIZG9jdW1lbnQYAiABKAlCC7pICHIGEAEogIBAIk8KGVB1dE1vZHVsZUVudmVsb3BlUmVzcG9uc2USMgoIZW52ZWxvcGUYASABKAsyIC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVudmVsb3BlKn4KEkNhdGFsb2d1ZUVudHJ5S2luZBIkCiBDQVRBTE9HVUVfRU5UUllfS0lORF9VTlNQRUNJRklFRBAAEh8KG0NBVEFMT0dVRV9FTlRSWV9LSU5EX01PRFVMRRABEiEKHUNBVEFMT0dVRV9FTlRSWV9LSU5EX1NPTFVUSU9OEAIqrwEKEkNhdGFsb2d1ZUdhcFJlYXNvbhIkCiBDQVRBTE9HVUVfR0FQX1JFQVNPTl9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRUNPUkRFRBABEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRVBPUlRFRBACEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9PQlNFUlZFRBADKt4BChhDYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3QSKgomQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfVU5TUEVDSUZJRUQQABIxCi1DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0FVVEhPUklaRUQQARIuCipDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0RJRkZFUlMQAhIzCi9DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX1VOQVVUSE9SSVpFRBADMvUiChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWUSqgEKEUdldE1vZHVsZUVudmVsb3BlEiouc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkdldE1vZHVsZUVudmVsb3BlUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL21vZHVsZS1lbnZlbG9wZRLWAQoRUHV0TW9kdWxlRW52ZWxvcGUSKi5zYWFzLmFjY291bnRzLnYxLlB1dE1vZHVsZUVudmVsb3BlUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXNwb25zZSJowvMYPQgCEAEwAzopCiVzYWFzLnBsYXRmb3JtLm1vZHVsZV9lbnZlbG9wZV93cml0dGVuEAJAAUgFUANYA2AFeAKC0+STAiE6ASoaHC92MS9wbGF0Zm9ybS9tb2R1bGUtZW52ZWxvcGViBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCJhChlHZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjcKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZUgAiAEBQgsKCV9lbnZlbG9wZSJdChhQdXRNb2R1bGVFbnZlbG9wZVJlcXVlc3QSIgoRZXhwZWN0ZWRfcmV2aXNpb24YASABKANCB7pIBCICKAASHQoIZG9jdW1lbnQYAiABKAlCC7pICHIGEAEogIBAIk8KGVB1dE1vZHVsZUVudmVsb3BlUmVzcG9uc2USMgoIZW52ZWxvcGUYASABKAsyIC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVudmVsb3BlIqIBCiJSZXNvbHZlTW9kdWxlVGVuYW50SWRlbnRpdHlSZXF1ZXN0EjwKBnByZWZpeBgBIAEoCUIsukgpcicQARg/MiFeW2EtejAtOV0oPzpbYS16MC05LV0qW2EtejAtOV0pPyQSGwoHYmluZGluZxgCIAEoCUIKukgHcgUQARiAARIhCg9vcmdhbml6YXRpb25faWQYAyABKAlCCLpIBXIDsAEBIpoBChRNb2R1bGVUZW5hbnRJZGVudGl0eRIUCgxwcmluY2lwYWxfaWQYASABKAkSDgoGdGVuYW50GAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEg4KBnNjb3BlcxgEIAMoCRIYChBsaWZldGltZV9zZWNvbmRzGAUgASgDEhAKCHJldmlzaW9uGAYgASgEEg4KBnNoYXJlZBgHIAEoCCKcAQoYTW9kdWxlVGVuYW50SWRlbnRpdHlNaW50Eg4KBnJlY29yZBgBIAEoCRIOCgZ0ZW5hbnQYAiABKAkSDwoHYmluZGluZxgDIAEoCRIQCghhdWRpZW5jZRgEIAEoCRIOCgZzY29wZXMYBSADKAkSLQoJbWludGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKsAQojUmVzb2x2ZU1vZHVsZVRlbmFudElkZW50aXR5UmVzcG9uc2USOAoIaWRlbnRpdHkYASABKAsyJi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVRlbmFudElkZW50aXR5Eg8KB3JlZnVzYWwYAiABKAkSOgoGcmVjZW50GAMgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVUZW5hbnRJZGVudGl0eU1pbnQqfgoSQ2F0YWxvZ3VlRW50cnlLaW5kEiQKIENBVEFMT0dVRV9FTlRSWV9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQ0FUQUxPR1VFX0VOVFJZX0tJTkRfTU9EVUxFEAESIQodQ0FUQUxPR1VFX0VOVFJZX0tJTkRfU09MVVRJT04QAiqvAQoSQ2F0YWxvZ3VlR2FwUmVhc29uEiQKIENBVEFMT0dVRV9HQVBfUkVBU09OX1VOU1BFQ0lGSUVEEAASJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFQ09SREVEEAESJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFUE9SVEVEEAISJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX09CU0VSVkVEEAMq3gEKGENhdGFsb2d1ZU9ic2VydmVkVmVyZGljdBIqCiZDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9VTlNQRUNJRklFRBAAEjEKLUNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfQVVUSE9SSVpFRBABEi4KKkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfRElGRkVSUxACEjMKL0NBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfVU5BVVRIT1JJWkVEEAMyxyQKFFBsYXRmb3JtQWRtaW5TZXJ2aWNlEo4BCgtTZWFyY2hVc2VycxIkLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1Jlc3BvbnNlIjLC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhQSEi92MS9wbGF0Zm9ybS91c2VycxKrAQoLU3VzcGVuZFVzZXISJC5zYWFzLmFjY291bnRzLnYxLlN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJewvMYKwgCEAEwAToXChNzYWFzLnVzZXIuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAik6ASoiJC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06c3VzcGVuZBKzAQoNVW5zdXNwZW5kVXNlchImLnNhYXMuYWNjb3VudHMudjEuVW5zdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiYsLzGC0IAhABMAE6GQoVc2Fhcy51c2VyLnVuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAis6ASoiJi92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06dW5zdXNwZW5kEtYBCg9JbXBlcnNvbmF0ZVVzZXISKC5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlc3BvbnNlIm7C8xg3CAIQATAEOiMKH3NhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0ZWQQAkABSAVQA1gEYAN4AoLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTppbXBlcnNvbmF0ZRLYAQoRU3RvcEltcGVyc29uYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXNwb25zZSJqwvMYPAgCEAEwAToqCiZzYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGlvbl9lbmRlZBACQAFIBVADWANgAYLT5JMCJDoBKiIfL3YxL3BsYXRmb3JtL2ltcGVyc29uYXRpb246c3RvcBKmAQoSTGlzdEFjdGl2ZVNlc3Npb25zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXNwb25zZSI1wvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIXEhUvdjEvcGxhdGZvcm0vc2Vzc2lvbnMSqQEKDVJldm9rZVNlc3Npb24SJi5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNlc3Npb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IljC8xgqCAIQATABOhgKFHNhYXMuc2Vzc2lvbi5yZXZva2VkEAJAAUgFUANYA2ADgtPkkwIkKiIvdjEvcGxhdGZvcm0vc2Vzc2lvbnMve3Nlc3Npb25faWR9Es8BChJHZXRPcmdFbnRpdGxlbWVudHMSKy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1Jlc3BvbnNlIl7C8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCMhIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEuQBChNPdmVycmlkZUVudGl0bGVtZW50Eiwuc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlInDC8xgxCAIQATABOh0KGXNhYXMuZW50aXRsZW1lbnQub3ZlcnJpZGUQAkABSAVQA1gDYAJ4AoLT5JMCNToBKiIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEq0BChFHcmFudFBsYXRmb3JtUm9sZRIqLnNhYXMuYWNjb3VudHMudjEuR3JhbnRQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlTC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9ncmFudGVkEAJAAUgFUANYA2AFeAKC0+STAhg6ASoiEy92MS9wbGF0Zm9ybS9hZG1pbnMStgEKElJldm9rZVBsYXRmb3JtUm9sZRIrLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJbwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfcmV2b2tlZBACQAFIBVADWANgBXgCgtPkkwIfKh0vdjEvcGxhdGZvcm0vYWRtaW5zL3t1c2VyX2lkfRKkAQoSTGlzdFBsYXRmb3JtQWRtaW5zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXNwb25zZSIzwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIVEhMvdjEvcGxhdGZvcm0vYWRtaW5zEo8BChRMaXN0QWxsT3JnYW5pemF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSkgEKFUdldE9yZ2FuaXphdGlvblJvc3RlchIuLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKlAQoQTGlzdEZlYXR1cmVGbGFncxIpLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncxLSAQoRVXBzZXJ0RmVhdHVyZUZsYWcSKi5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXNwb25zZSJkiAIBwvMYMQgCEAEwATodChlzYWFzLmZlYXR1cmVfZmxhZy51cGRhdGVkEAJAAUgFUANYA2AFeAKC0+STAiY6ASoaIS92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzL3tuYW1lfRKfAQoQR2V0Sm9iT3BlcmF0aW9ucxIlLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVxdWVzdBomLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL2pvYnMvb3BlcmF0aW9ucxJ8CghMaXN0Sm9icxIdLnNhYXMuam9icy52MS5MaXN0Sm9ic1JlcXVlc3QaHi5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXNwb25zZSIxwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwITEhEvdjEvcGxhdGZvcm0vam9icxJ/CgZHZXRKb2ISGy5zYWFzLmpvYnMudjEuR2V0Sm9iUmVxdWVzdBocLnNhYXMuam9icy52MS5HZXRKb2JSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vam9icy97am9iX2lkfRKsAQoJUmVwbGF5Sm9iEh4uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlcXVlc3QaHy5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVzcG9uc2UiXsLzGCcIAhABMAM6FQoRc2Fhcy5qb2IucmVwbGF5ZWQQAkADSAVQA1gDYAWC0+STAi06ASoiKC92MS9wbGF0Zm9ybS9qb2JzL3tzb3VyY2Vfam9iX2lkfTpyZXBsYXkSqwEKEkdldEV2ZW50T3BlcmF0aW9ucxIpLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1JlcXVlc3QaKi5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXNwb25zZSI+wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIgEh4vdjEvcGxhdGZvcm0vZXZlbnRzL29wZXJhdGlvbnMSugEKFkxpc3RFdmVudFN1YnNjcmlwdGlvbnMSLS5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVxdWVzdBouLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXNwb25zZSJBwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIjEiEvdjEvcGxhdGZvcm0vZXZlbnRzL3N1YnNjcmlwdGlvbnMSsAEKFUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZRIuLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2UiNsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCGBIWL3YxL3BsYXRmb3JtL2NhdGFsb2d1ZRKqAQoRR2V0TW9kdWxlRW52ZWxvcGUSKi5zYWFzLmFjY291bnRzLnYxLkdldE1vZHVsZUVudmVsb3BlUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEtYBChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlImjC8xg9CAIQATADOikKJXNhYXMucGxhdGZvcm0ubW9kdWxlX2VudmVsb3BlX3dyaXR0ZW4QAkABSAVQA1gDYAV4AoLT5JMCIToBKhocL3YxL3BsYXRmb3JtL21vZHVsZS1lbnZlbG9wZRLPAQobUmVzb2x2ZU1vZHVsZVRlbmFudElkZW50aXR5EjQuc2Fhcy5hY2NvdW50cy52MS5SZXNvbHZlTW9kdWxlVGVuYW50SWRlbnRpdHlSZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5SZXNvbHZlTW9kdWxlVGVuYW50SWRlbnRpdHlSZXNwb25zZSJDwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIlEiMvdjEvcGxhdGZvcm0vbW9kdWxlLXRlbmFudC1pZGVudGl0eWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -1646,6 +1646,191 @@ export type PutModuleEnvelopeResponse = Message<"saas.accounts.v1.PutModuleEnvel export const PutModuleEnvelopeResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 51); +/** + * @generated from message saas.accounts.v1.ResolveModuleTenantIdentityRequest + */ +export type ResolveModuleTenantIdentityRequest = Message<"saas.accounts.v1.ResolveModuleTenantIdentityRequest"> & { + /** + * prefix is the module principal's registration prefix and binding the key + * of its operation_audiences entry. + * + * @generated from field: string prefix = 1; + */ + prefix: string; + + /** + * @generated from field: string binding = 2; + */ + binding: string; + + /** + * organization_id is the tenant the capability would act for. + * + * @generated from field: string organization_id = 3; + */ + organizationId: string; +}; + +/** + * Describes the message saas.accounts.v1.ResolveModuleTenantIdentityRequest. + * Use `create(ResolveModuleTenantIdentityRequestSchema)` to create a new message. + */ +export const ResolveModuleTenantIdentityRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 52); + +/** + * ModuleTenantIdentity is what one per-tenant mint would assert. Every field is + * the host's: nothing here is requestable. + * + * @generated from message saas.accounts.v1.ModuleTenantIdentity + */ +export type ModuleTenantIdentity = Message<"saas.accounts.v1.ModuleTenantIdentity"> & { + /** + * principal_id is the module's own service principal, which owns the + * capability and acts directly — there is no person and no actor hop. + * + * @generated from field: string principal_id = 1; + */ + principalId: string; + + /** + * tenant is the organization it would act for, and audience the service the + * binding declares it may call. A binding whose audience is the module's own + * prefix is one the module admits itself, which is how unattended work is + * admitted by the module that executes it. + * + * @generated from field: string tenant = 2; + */ + tenant: string; + + /** + * @generated from field: string audience = 3; + */ + audience: string; + + /** + * scopes is exactly what the capability would carry, as + * `kind:action[:resource]`: the binding's headless_scopes narrowed by THIS + * organization's installation scope. Never its invoke_scopes, which describe + * authority exercised on a person's behalf. + * + * @generated from field: repeated string scopes = 4; + */ + scopes: string[]; + + /** + * lifetime_seconds is the fixed lifetime every such capability carries. + * + * @generated from field: int64 lifetime_seconds = 5; + */ + lifetimeSeconds: bigint; + + /** + * revision is the authorization revision it would be sealed with: a digest + * of the module's whole effective entry, so any change to the entry stops + * confirming every capability minted under the old one. + * + * @generated from field: uint64 revision = 6; + */ + revision: bigint; + + /** + * shared says the principal is unpinned and declares cross_tenant, so it may + * act for more than one organization. A pinned principal reads false and can + * only ever name the tenant its envelope declares. + * + * @generated from field: bool shared = 7; + */ + shared: boolean; +}; + +/** + * Describes the message saas.accounts.v1.ModuleTenantIdentity. + * Use `create(ModuleTenantIdentitySchema)` to create a new message. + */ +export const ModuleTenantIdentitySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 53); + +/** + * ModuleTenantIdentityMint is one capability actually issued, as the audit + * trail recorded it. There is no other record: a capability lives a minute and + * nothing caches one. + * + * @generated from message saas.accounts.v1.ModuleTenantIdentityMint + */ +export type ModuleTenantIdentityMint = Message<"saas.accounts.v1.ModuleTenantIdentityMint"> & { + /** + * @generated from field: string record = 1; + */ + record: string; + + /** + * @generated from field: string tenant = 2; + */ + tenant: string; + + /** + * @generated from field: string binding = 3; + */ + binding: string; + + /** + * @generated from field: string audience = 4; + */ + audience: string; + + /** + * @generated from field: repeated string scopes = 5; + */ + scopes: string[]; + + /** + * @generated from field: google.protobuf.Timestamp minted_at = 6; + */ + mintedAt?: Timestamp; +}; + +/** + * Describes the message saas.accounts.v1.ModuleTenantIdentityMint. + * Use `create(ModuleTenantIdentityMintSchema)` to create a new message. + */ +export const ModuleTenantIdentityMintSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 54); + +/** + * @generated from message saas.accounts.v1.ResolveModuleTenantIdentityResponse + */ +export type ResolveModuleTenantIdentityResponse = Message<"saas.accounts.v1.ResolveModuleTenantIdentityResponse"> & { + /** + * identity is set when a mint for this organization would be issued; refusal + * says why it would not. Exactly one is present, and a refusal is a decision + * about declared authority rather than an outage. + * + * @generated from field: saas.accounts.v1.ModuleTenantIdentity identity = 1; + */ + identity?: ModuleTenantIdentity; + + /** + * @generated from field: string refusal = 2; + */ + refusal: string; + + /** + * recent is the trail of capabilities this module was actually issued for + * this organization, newest first. + * + * @generated from field: repeated saas.accounts.v1.ModuleTenantIdentityMint recent = 3; + */ + recent: ModuleTenantIdentityMint[]; +}; + +/** + * Describes the message saas.accounts.v1.ResolveModuleTenantIdentityResponse. + * Use `create(ResolveModuleTenantIdentityResponseSchema)` to create a new message. + */ +export const ResolveModuleTenantIdentityResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 55); + /** * CatalogueEntryKind distinguishes a composed module from a solution. * @@ -2018,5 +2203,17 @@ export const PlatformAdminService: GenService<{ input: typeof PutModuleEnvelopeRequestSchema; output: typeof PutModuleEnvelopeResponseSchema; }, + /** + * ResolveModuleTenantIdentity answers what a per-tenant module operation + * context would assert for one module, one of its bindings and one + * organization — or why it would be refused. It issues no capability. + * + * @generated from rpc saas.accounts.v1.PlatformAdminService.ResolveModuleTenantIdentity + */ + resolveModuleTenantIdentity: { + methodKind: "unary"; + input: typeof ResolveModuleTenantIdentityRequestSchema; + output: typeof ResolveModuleTenantIdentityResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_saas_accounts_v1_platform_admin, 0); diff --git a/module/services/frontend/code/packages/saas-sdk/package.json b/module/services/frontend/code/packages/saas-sdk/package.json index 8357b1dcb..df0bfe953 100644 --- a/module/services/frontend/code/packages/saas-sdk/package.json +++ b/module/services/frontend/code/packages/saas-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/saas-sdk", - "version": "0.3.29", + "version": "0.3.30", "description": "Typed Connect client, gateway-bound facades, and audit data-graph tooling for the saas-starter public API.", "repository": { "type": "git", diff --git a/module/services/frontend/code/src/app/admin/platform/module-identity/page.tsx b/module/services/frontend/code/src/app/admin/platform/module-identity/page.tsx new file mode 100644 index 000000000..7628cee5f --- /dev/null +++ b/module/services/frontend/code/src/app/admin/platform/module-identity/page.tsx @@ -0,0 +1,34 @@ +import { RoleGate } from "@/components/auth/role-gate"; +import { ModuleTenantIdentityPlayground } from "@/features/module-tenant-identity"; + +function AccessDenied() { + return ( +
+

+ Super administrator required +

+

+ Resolving what a module's capability would carry reads any + organization's installation scope and is restricted to platform + super administrators. +

+
+ ); +} + +/** + * The playground for a module identity minted for one tenant. + * + * Under `/admin/platform/`, which is where it belongs and where the route + * catalog derives `super_admin` from — the same boundary `ResolveModuleTenantIdentity` + * declares. It reads any organization's installation scope, which is + * cross-tenant authority however read-only it is, so this page's gate and the + * RPC's `platform_role` are one answer rather than two. + */ +export default function Page() { + return ( + }> + + + ); +} diff --git a/module/services/frontend/code/src/app/admin/platform/page.tsx b/module/services/frontend/code/src/app/admin/platform/page.tsx index d371b2d09..2cfbaba2f 100644 --- a/module/services/frontend/code/src/app/admin/platform/page.tsx +++ b/module/services/frontend/code/src/app/admin/platform/page.tsx @@ -80,6 +80,13 @@ const sections = [ href: "/admin/unattended-work", icon: KeyRound, }, + { + title: "Module Identity", + description: + "Resolve what a module's capability for one tenant would carry, and why it would be refused.", + href: "/admin/platform/module-identity", + icon: KeyRound, + }, { title: "Plugin Transport", description: diff --git a/module/services/frontend/code/src/features/module-tenant-identity/index.ts b/module/services/frontend/code/src/features/module-tenant-identity/index.ts new file mode 100644 index 000000000..875755e8c --- /dev/null +++ b/module/services/frontend/code/src/features/module-tenant-identity/index.ts @@ -0,0 +1,2 @@ +export { useModuleTenantIdentity } from "./service/queries"; +export { ModuleTenantIdentityPlayground } from "./ui/module-tenant-identity-playground"; diff --git a/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts new file mode 100644 index 000000000..c1dfef14d --- /dev/null +++ b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts @@ -0,0 +1,29 @@ +import { useQuery } from "@tanstack/react-query"; +import { usePlatformAdminService } from "@/lib/hooks/use-api-client"; + +/** + * The read behind the per-tenant module identity playground. + * + * It resolves what one module's capability WOULD assert for one organization, + * and issues nothing: the mint needs the module pod's projected ServiceAccount + * token held against the build its authority document approves, which a browser + * has none of — and a path that answered an operator with a module's capability + * would be a second spelling of that module's identity. + * + * `enabled` is every field, because the answer is about the triple. A read with + * two of the three would be asking a different question and the page would show + * its answer as if it were this one's. + */ +export function useModuleTenantIdentity( + prefix: string, + binding: string, + organizationId: string, +) { + const service = usePlatformAdminService(); + return useQuery({ + queryKey: ["module-tenant-identity", prefix, binding, organizationId], + enabled: !!prefix && !!binding && !!organizationId, + queryFn: () => + service.resolveModuleTenantIdentity({ prefix, binding, organizationId }), + }); +} diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx new file mode 100644 index 000000000..d07b194fb --- /dev/null +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx @@ -0,0 +1,156 @@ +import { cleanup, fireEvent, screen, within } from "@testing-library/react"; +import { HttpResponse, http, type JsonBodyType } from "msw"; +import { afterEach, describe, expect, it } from "vitest"; +import { renderInApp, rpc } from "@/test/container"; +import { server } from "@/test/setup"; +import { ModuleTenantIdentityPlayground } from "./module-tenant-identity-playground"; + +// THE PAGE RESOLVES A MINT AND NEVER PERFORMS ONE. +// +// That is the claim worth testing, and it is a claim about what the page asks +// for rather than about what renders: a surface that answered an operator with +// a module's capability would be a second spelling of that module's identity. +// So the test records the requests the page makes, asserts they are the +// resolution and nothing else, and then asserts the two answers an operator +// has to tell apart — what would be carried, and why nothing would be. + +afterEach(cleanup); + +const ORG = "11111111-1111-4111-8111-111111111111"; + +const minted = { + identity: { + principalId: "module:example", + tenant: ORG, + audience: "example.api", + scopes: ["records:read:one", "records:write:one"], + lifetimeSeconds: "60", + revision: "8123456789", + shared: true, + }, + refusal: "", + recent: [ + { + record: "audit-1", + tenant: ORG, + binding: "operate", + audience: "example.api", + scopes: ["records:read:one"], + mintedAt: "2026-10-13T09:00:00Z", + }, + ], +}; + +const refused = { + refusal: "the organization has no live installation of this module", + recent: [], +}; + +/** Serve the resolution, and record every request the page makes. */ +function serve(answer: JsonBodyType): { calls: unknown[] } { + const calls: unknown[] = []; + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + async ({ request }) => { + calls.push(await request.json()); + return HttpResponse.json(answer); + }, + ), + ); + return { calls }; +} + +function fill() { + fireEvent.change(screen.getByLabelText("Module prefix"), { + target: { value: "example" }, + }); + fireEvent.change(screen.getByLabelText("Operation binding"), { + target: { value: "operate" }, + }); + fireEvent.change(screen.getByLabelText("Organization"), { + target: { value: ORG }, + }); +} + +describe("ModuleTenantIdentityPlayground", () => { + it("asks nothing until the triple is named", async () => { + const recorded = serve(minted); + renderInApp(); + + // The answer is about the three together, so two of them is a different + // question — and showing its answer as this one's would be wrong. + fireEvent.change(screen.getByLabelText("Module prefix"), { + target: { value: "example" }, + }); + fireEvent.change(screen.getByLabelText("Operation binding"), { + target: { value: "operate" }, + }); + expect( + await screen.findByText(/Name a module, a binding and an organization/), + ).toBeTruthy(); + expect(recorded.calls).toHaveLength(0); + }); + + it("shows what one tenant's capability would carry, and asks for no mint", async () => { + const recorded = serve(minted); + renderInApp(); + fill(); + + expect( + await screen.findByText(/A capability would be issued/), + ).toBeTruthy(); + // Acting FOR the organization named: the tenant is the capability's, and + // it is what a consumer admitting a publication reads it from. + // Scoped to the resolution's own card: the audience also appears in the + // trail below it, and `getByText` over the page would be ambiguous for + // the wrong reason. + const resolved = document.querySelector( + '[data-slot="module-tenant-identity"]', + ) as HTMLElement; + expect(within(resolved).getByText(ORG)).toBeTruthy(); + expect(within(resolved).getByText("example.api")).toBeTruthy(); + expect(within(resolved).getByText("60 seconds")).toBeTruthy(); + // The scopes are the binding's headless list as this organization's + // installation narrowed it — never its invoke scopes. + expect( + within(resolved).getByText("records:read:one, records:write:one"), + ).toBeTruthy(); + expect( + screen.getByText(/may act for any organization that installs it/), + ).toBeTruthy(); + + // The trail of capabilities actually issued, which is the only record of + // one: nothing caches a capability. + expect(document.querySelectorAll('[data-slot="module-tenant-mint"]')).toHaveLength(1); + + // And the whole point: one request, the resolution, carrying the triple + // — and no mint anywhere. + expect(recorded.calls).toEqual([ + { prefix: "example", binding: "operate", organizationId: ORG }, + ]); + }); + + it("shows the refusal in the host's own words", async () => { + // A refusal is a decision about declared authority, and each one is a + // different thing for an operator to do about it — so the page prints + // what the host said rather than mapping every refusal onto one word. + serve(refused); + renderInApp(); + fill(); + + expect( + await screen.findByText(/No capability would be issued/), + ).toBeTruthy(); + expect( + screen.getByText(/has no live installation of this module/), + ).toBeTruthy(); + expect( + screen.getByText(/is a decision about declared authority, not an outage/), + ).toBeTruthy(); + // A refused resolution shows no capability's facts at all. + expect( + document.querySelector('[data-slot="module-tenant-identity"]'), + ).toBeNull(); + }); +}); diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx new file mode 100644 index 000000000..aab892d13 --- /dev/null +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx @@ -0,0 +1,316 @@ +"use client"; + +import { timestampDate } from "@bufbuild/protobuf/wkt"; +import { KeyRound } from "lucide-react"; +import { useState } from "react"; +import { EmptyState } from "@/components/empty-state"; +import { + readOutcome, + readOutcomeMessage, +} from "@/shared/lib/read-outcome"; +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + DelayedLoading, + Input, + Label, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui"; +import { useModuleTenantIdentity } from "../service/queries"; + +/** + * The playground for a module identity minted for ONE tenant. + * + * A composed module acting with no person present mints a Work Context for one + * of its installed operation bindings and names the organization it is acting + * FOR. That capability is how a module presents itself as acting for a tenant + * rather than as itself, and it is what a consumer admitting a publication for + * a tenant needs from a publisher with nobody signed in: the tenant comes from + * the verified capability, where this host has already checked that the + * principal may speak for it, never from a request field. + * + * Three things bound it, and the page shows each of them answered: + * + * - the ENVELOPE decides which organizations may be named. A principal pinned + * to a tenant may name that tenant and no other; an unpinned principal + * declaring `cross_tenant` may name one organization per mint. Naming an + * organization is not authority over it. + * - that organization's own live INSTALLATION must consent to this module. + * - the binding must declare `headless_scopes`, and what the capability + * carries is that list narrowed by THIS organization's installation scope. + * An organization that consented to less gets less, and one that consented + * to nothing is refused — which is what makes the bound per tenant rather + * than per deployment. + * + * IT MINTS NOTHING, deliberately. The mint authenticates with the module pod's + * projected ServiceAccount token, held against the build its authority document + * approves; a browser holds no such token, and a surface that handed an operator + * a module's capability would be a second spelling of that module's identity. So + * this resolves the decision from the same declared facts the mint decides on + * and stops one step before the signature. What it then shows beside the + * resolution is the trail of capabilities actually issued, which is the only + * record of one: a capability lives about a minute and nothing caches it. + */ +export function ModuleTenantIdentityPlayground() { + const [prefix, setPrefix] = useState(""); + const [binding, setBinding] = useState(""); + const [organizationId, setOrganizationId] = useState(""); + const read = useModuleTenantIdentity(prefix, binding, organizationId); + // `empty` is this classifier's word for "the read completed" — there is no + // list here, so the only thing it is asked is whether the answer arrived. + const outcome = readOutcome(read.isError, read.error); + const answer = read.data; + // A disabled query is not a loading one: with a field empty it sits at + // isPending with an idle fetch status, and a wait for a read that was never + // issued is a spinner that never resolves. + const waiting = read.isPending && read.fetchStatus !== "idle"; + + return ( +
+
+

+ Module identity for one tenant +

+

+ A composed module with no person present mints a capability for one of + its installed operation bindings and names the organization it acts + for. Resolve what that capability would carry — and why it would be + refused. +

+
+ + + + What would be minted + + Name a module's registration prefix, one of its operation + bindings, and the organization it would act for. This page reads the + decision and issues no capability: a mint authenticates with the + module pod's projected ServiceAccount token, which a browser + does not have and must not be given a way around. + + + +
+
+ + setPrefix(event.target.value.trim())} + /> +
+
+ + setBinding(event.target.value.trim())} + /> +
+
+ + + setOrganizationId(event.target.value.trim()) + } + /> +
+ +
+ + {waiting ? ( + + ) : outcome !== "empty" ? ( +

+ {readOutcomeMessage(outcome, "this module's declared authority", "")} +

+ ) : !answer ? ( + + ) : answer.identity ? ( + + ) : ( + + )} +
+
+ + + + Capabilities issued + + Every mint is recorded, and the record is the only trace: a + capability lives about a minute and nothing caches one. A reference + that no longer resolves shows up as the next mint having no row + here at all. + + + + {!answer || answer.recent.length === 0 ? ( +

+ No capability has been issued to this module for this + organization. +

+ ) : ( + + + + Minted + Binding + Audience + Scopes + Record + + + + {answer.recent.map((mint) => ( + + + {mint.mintedAt + ? timestampDate(mint.mintedAt).toLocaleString() + : "—"} + + + {mint.binding} + + + {mint.audience} + + + {mint.scopes.join(", ") || "—"} + + + {mint.record} + + + ))} + +
+ )} +
+
+
+ ); +} + +/** What one per-tenant capability would assert, field by field. */ +function Minted({ + identity, +}: { + identity: { + principalId: string; + tenant: string; + audience: string; + scopes: readonly string[]; + lifetimeSeconds: bigint; + revision: bigint; + shared: boolean; + }; +}) { + return ( +
+ A capability would be issued +
+ + + + + + +
+
+

+ Scopes, as this organization's installation narrowed them +

+

+ {identity.scopes.join(", ") || "—"} +

+
+

+ The owner is the module's own service principal and it acts + directly: no person, and no actor hop. The scopes are the + binding's headless_scopes narrowed by this + organization's installation scope, never the{" "} + invoke_scopes it holds for work a person asked for. +

+
+ ); +} + +/** + * Why nothing would be minted. The refusal is the host's own words rather than + * a code, because each one is a different thing for an operator to do: declare + * the principal, widen the envelope's tenancy, install the module in that + * organization, or consent to the binding's headless scopes there. + */ +function Refused({ refusal }: { refusal: string }) { + return ( +
+ No capability would be issued +

{refusal}

+

+ A refusal here is a decision about declared authority, not an outage: it + is what the mint itself would answer, and retrying changes nothing until + the declaration does. +

+
+ ); +} + +/** One labelled fact. */ +function Fact({ + label, + value, + mono, +}: { + label: string; + value: string; + mono?: boolean; +}) { + return ( +
+
{label}
+
+ {value} +
+
+ ); +} diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts index d93916ac5..e0aa61a8a 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCJhChlHZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjcKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZUgAiAEBQgsKCV9lbnZlbG9wZSJdChhQdXRNb2R1bGVFbnZlbG9wZVJlcXVlc3QSIgoRZXhwZWN0ZWRfcmV2aXNpb24YASABKANCB7pIBCICKAASHQoIZG9jdW1lbnQYAiABKAlCC7pICHIGEAEogIBAIk8KGVB1dE1vZHVsZUVudmVsb3BlUmVzcG9uc2USMgoIZW52ZWxvcGUYASABKAsyIC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVudmVsb3BlKn4KEkNhdGFsb2d1ZUVudHJ5S2luZBIkCiBDQVRBTE9HVUVfRU5UUllfS0lORF9VTlNQRUNJRklFRBAAEh8KG0NBVEFMT0dVRV9FTlRSWV9LSU5EX01PRFVMRRABEiEKHUNBVEFMT0dVRV9FTlRSWV9LSU5EX1NPTFVUSU9OEAIqrwEKEkNhdGFsb2d1ZUdhcFJlYXNvbhIkCiBDQVRBTE9HVUVfR0FQX1JFQVNPTl9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRUNPUkRFRBABEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRVBPUlRFRBACEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9PQlNFUlZFRBADKt4BChhDYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3QSKgomQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfVU5TUEVDSUZJRUQQABIxCi1DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0FVVEhPUklaRUQQARIuCipDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0RJRkZFUlMQAhIzCi9DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX1VOQVVUSE9SSVpFRBADMvUiChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWUSqgEKEUdldE1vZHVsZUVudmVsb3BlEiouc2Fhcy5hY2NvdW50cy52MS5HZXRNb2R1bGVFbnZlbG9wZVJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkdldE1vZHVsZUVudmVsb3BlUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL21vZHVsZS1lbnZlbG9wZRLWAQoRUHV0TW9kdWxlRW52ZWxvcGUSKi5zYWFzLmFjY291bnRzLnYxLlB1dE1vZHVsZUVudmVsb3BlUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXNwb25zZSJowvMYPQgCEAEwAzopCiVzYWFzLnBsYXRmb3JtLm1vZHVsZV9lbnZlbG9wZV93cml0dGVuEAJAAUgFUANYA2AFeAKC0+STAiE6ASoaHC92MS9wbGF0Zm9ybS9tb2R1bGUtZW52ZWxvcGVCugEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhJQbGF0Zm9ybUFkbWluUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASJ4Cg5Nb2R1bGVFbnZlbG9wZRIQCghyZXZpc2lvbhgBIAEoAxIQCghkb2N1bWVudBgCIAEoCRISCgp3cml0dGVuX2J5GAMgASgJEi4KCndyaXR0ZW5fYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKGEdldE1vZHVsZUVudmVsb3BlUmVxdWVzdCJhChlHZXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlEjcKCGVudmVsb3BlGAEgASgLMiAuc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVFbnZlbG9wZUgAiAEBQgsKCV9lbnZlbG9wZSJdChhQdXRNb2R1bGVFbnZlbG9wZVJlcXVlc3QSIgoRZXhwZWN0ZWRfcmV2aXNpb24YASABKANCB7pIBCICKAASHQoIZG9jdW1lbnQYAiABKAlCC7pICHIGEAEogIBAIk8KGVB1dE1vZHVsZUVudmVsb3BlUmVzcG9uc2USMgoIZW52ZWxvcGUYASABKAsyIC5zYWFzLmFjY291bnRzLnYxLk1vZHVsZUVudmVsb3BlIqIBCiJSZXNvbHZlTW9kdWxlVGVuYW50SWRlbnRpdHlSZXF1ZXN0EjwKBnByZWZpeBgBIAEoCUIsukgpcicQARg/MiFeW2EtejAtOV0oPzpbYS16MC05LV0qW2EtejAtOV0pPyQSGwoHYmluZGluZxgCIAEoCUIKukgHcgUQARiAARIhCg9vcmdhbml6YXRpb25faWQYAyABKAlCCLpIBXIDsAEBIpoBChRNb2R1bGVUZW5hbnRJZGVudGl0eRIUCgxwcmluY2lwYWxfaWQYASABKAkSDgoGdGVuYW50GAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEg4KBnNjb3BlcxgEIAMoCRIYChBsaWZldGltZV9zZWNvbmRzGAUgASgDEhAKCHJldmlzaW9uGAYgASgEEg4KBnNoYXJlZBgHIAEoCCKcAQoYTW9kdWxlVGVuYW50SWRlbnRpdHlNaW50Eg4KBnJlY29yZBgBIAEoCRIOCgZ0ZW5hbnQYAiABKAkSDwoHYmluZGluZxgDIAEoCRIQCghhdWRpZW5jZRgEIAEoCRIOCgZzY29wZXMYBSADKAkSLQoJbWludGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKsAQojUmVzb2x2ZU1vZHVsZVRlbmFudElkZW50aXR5UmVzcG9uc2USOAoIaWRlbnRpdHkYASABKAsyJi5zYWFzLmFjY291bnRzLnYxLk1vZHVsZVRlbmFudElkZW50aXR5Eg8KB3JlZnVzYWwYAiABKAkSOgoGcmVjZW50GAMgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Nb2R1bGVUZW5hbnRJZGVudGl0eU1pbnQqfgoSQ2F0YWxvZ3VlRW50cnlLaW5kEiQKIENBVEFMT0dVRV9FTlRSWV9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQ0FUQUxPR1VFX0VOVFJZX0tJTkRfTU9EVUxFEAESIQodQ0FUQUxPR1VFX0VOVFJZX0tJTkRfU09MVVRJT04QAiqvAQoSQ2F0YWxvZ3VlR2FwUmVhc29uEiQKIENBVEFMT0dVRV9HQVBfUkVBU09OX1VOU1BFQ0lGSUVEEAASJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFQ09SREVEEAESJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFUE9SVEVEEAISJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX09CU0VSVkVEEAMq3gEKGENhdGFsb2d1ZU9ic2VydmVkVmVyZGljdBIqCiZDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9VTlNQRUNJRklFRBAAEjEKLUNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfQVVUSE9SSVpFRBABEi4KKkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfRElGRkVSUxACEjMKL0NBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfVU5BVVRIT1JJWkVEEAMyxyQKFFBsYXRmb3JtQWRtaW5TZXJ2aWNlEo4BCgtTZWFyY2hVc2VycxIkLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1Jlc3BvbnNlIjLC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhQSEi92MS9wbGF0Zm9ybS91c2VycxKrAQoLU3VzcGVuZFVzZXISJC5zYWFzLmFjY291bnRzLnYxLlN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJewvMYKwgCEAEwAToXChNzYWFzLnVzZXIuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAik6ASoiJC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06c3VzcGVuZBKzAQoNVW5zdXNwZW5kVXNlchImLnNhYXMuYWNjb3VudHMudjEuVW5zdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiYsLzGC0IAhABMAE6GQoVc2Fhcy51c2VyLnVuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAis6ASoiJi92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06dW5zdXNwZW5kEtYBCg9JbXBlcnNvbmF0ZVVzZXISKC5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlc3BvbnNlIm7C8xg3CAIQATAEOiMKH3NhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0ZWQQAkABSAVQA1gEYAN4AoLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTppbXBlcnNvbmF0ZRLYAQoRU3RvcEltcGVyc29uYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXNwb25zZSJqwvMYPAgCEAEwAToqCiZzYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGlvbl9lbmRlZBACQAFIBVADWANgAYLT5JMCJDoBKiIfL3YxL3BsYXRmb3JtL2ltcGVyc29uYXRpb246c3RvcBKmAQoSTGlzdEFjdGl2ZVNlc3Npb25zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXNwb25zZSI1wvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIXEhUvdjEvcGxhdGZvcm0vc2Vzc2lvbnMSqQEKDVJldm9rZVNlc3Npb24SJi5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNlc3Npb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IljC8xgqCAIQATABOhgKFHNhYXMuc2Vzc2lvbi5yZXZva2VkEAJAAUgFUANYA2ADgtPkkwIkKiIvdjEvcGxhdGZvcm0vc2Vzc2lvbnMve3Nlc3Npb25faWR9Es8BChJHZXRPcmdFbnRpdGxlbWVudHMSKy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1Jlc3BvbnNlIl7C8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCMhIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEuQBChNPdmVycmlkZUVudGl0bGVtZW50Eiwuc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlInDC8xgxCAIQATABOh0KGXNhYXMuZW50aXRsZW1lbnQub3ZlcnJpZGUQAkABSAVQA1gDYAJ4AoLT5JMCNToBKiIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEq0BChFHcmFudFBsYXRmb3JtUm9sZRIqLnNhYXMuYWNjb3VudHMudjEuR3JhbnRQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlTC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9ncmFudGVkEAJAAUgFUANYA2AFeAKC0+STAhg6ASoiEy92MS9wbGF0Zm9ybS9hZG1pbnMStgEKElJldm9rZVBsYXRmb3JtUm9sZRIrLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJbwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfcmV2b2tlZBACQAFIBVADWANgBXgCgtPkkwIfKh0vdjEvcGxhdGZvcm0vYWRtaW5zL3t1c2VyX2lkfRKkAQoSTGlzdFBsYXRmb3JtQWRtaW5zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXNwb25zZSIzwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIVEhMvdjEvcGxhdGZvcm0vYWRtaW5zEo8BChRMaXN0QWxsT3JnYW5pemF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSkgEKFUdldE9yZ2FuaXphdGlvblJvc3RlchIuLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKlAQoQTGlzdEZlYXR1cmVGbGFncxIpLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncxLSAQoRVXBzZXJ0RmVhdHVyZUZsYWcSKi5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXNwb25zZSJkiAIBwvMYMQgCEAEwATodChlzYWFzLmZlYXR1cmVfZmxhZy51cGRhdGVkEAJAAUgFUANYA2AFeAKC0+STAiY6ASoaIS92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzL3tuYW1lfRKfAQoQR2V0Sm9iT3BlcmF0aW9ucxIlLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVxdWVzdBomLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL2pvYnMvb3BlcmF0aW9ucxJ8CghMaXN0Sm9icxIdLnNhYXMuam9icy52MS5MaXN0Sm9ic1JlcXVlc3QaHi5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXNwb25zZSIxwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwITEhEvdjEvcGxhdGZvcm0vam9icxJ/CgZHZXRKb2ISGy5zYWFzLmpvYnMudjEuR2V0Sm9iUmVxdWVzdBocLnNhYXMuam9icy52MS5HZXRKb2JSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vam9icy97am9iX2lkfRKsAQoJUmVwbGF5Sm9iEh4uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlcXVlc3QaHy5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVzcG9uc2UiXsLzGCcIAhABMAM6FQoRc2Fhcy5qb2IucmVwbGF5ZWQQAkADSAVQA1gDYAWC0+STAi06ASoiKC92MS9wbGF0Zm9ybS9qb2JzL3tzb3VyY2Vfam9iX2lkfTpyZXBsYXkSqwEKEkdldEV2ZW50T3BlcmF0aW9ucxIpLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1JlcXVlc3QaKi5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXNwb25zZSI+wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIgEh4vdjEvcGxhdGZvcm0vZXZlbnRzL29wZXJhdGlvbnMSugEKFkxpc3RFdmVudFN1YnNjcmlwdGlvbnMSLS5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVxdWVzdBouLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXNwb25zZSJBwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIjEiEvdjEvcGxhdGZvcm0vZXZlbnRzL3N1YnNjcmlwdGlvbnMSsAEKFUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZRIuLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2UiNsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCGBIWL3YxL3BsYXRmb3JtL2NhdGFsb2d1ZRKqAQoRR2V0TW9kdWxlRW52ZWxvcGUSKi5zYWFzLmFjY291bnRzLnYxLkdldE1vZHVsZUVudmVsb3BlUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuR2V0TW9kdWxlRW52ZWxvcGVSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vbW9kdWxlLWVudmVsb3BlEtYBChFQdXRNb2R1bGVFbnZlbG9wZRIqLnNhYXMuYWNjb3VudHMudjEuUHV0TW9kdWxlRW52ZWxvcGVSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5QdXRNb2R1bGVFbnZlbG9wZVJlc3BvbnNlImjC8xg9CAIQATADOikKJXNhYXMucGxhdGZvcm0ubW9kdWxlX2VudmVsb3BlX3dyaXR0ZW4QAkABSAVQA1gDYAV4AoLT5JMCIToBKhocL3YxL3BsYXRmb3JtL21vZHVsZS1lbnZlbG9wZRLPAQobUmVzb2x2ZU1vZHVsZVRlbmFudElkZW50aXR5EjQuc2Fhcy5hY2NvdW50cy52MS5SZXNvbHZlTW9kdWxlVGVuYW50SWRlbnRpdHlSZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5SZXNvbHZlTW9kdWxlVGVuYW50SWRlbnRpdHlSZXNwb25zZSJDwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIlEiMvdjEvcGxhdGZvcm0vbW9kdWxlLXRlbmFudC1pZGVudGl0eUK6AQoUY29tLnNhYXMuYWNjb3VudHMudjFCElBsYXRmb3JtQWRtaW5Qcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -1646,6 +1646,191 @@ export type PutModuleEnvelopeResponse = Message<"saas.accounts.v1.PutModuleEnvel export const PutModuleEnvelopeResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 51); +/** + * @generated from message saas.accounts.v1.ResolveModuleTenantIdentityRequest + */ +export type ResolveModuleTenantIdentityRequest = Message<"saas.accounts.v1.ResolveModuleTenantIdentityRequest"> & { + /** + * prefix is the module principal's registration prefix and binding the key + * of its operation_audiences entry. + * + * @generated from field: string prefix = 1; + */ + prefix: string; + + /** + * @generated from field: string binding = 2; + */ + binding: string; + + /** + * organization_id is the tenant the capability would act for. + * + * @generated from field: string organization_id = 3; + */ + organizationId: string; +}; + +/** + * Describes the message saas.accounts.v1.ResolveModuleTenantIdentityRequest. + * Use `create(ResolveModuleTenantIdentityRequestSchema)` to create a new message. + */ +export const ResolveModuleTenantIdentityRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 52); + +/** + * ModuleTenantIdentity is what one per-tenant mint would assert. Every field is + * the host's: nothing here is requestable. + * + * @generated from message saas.accounts.v1.ModuleTenantIdentity + */ +export type ModuleTenantIdentity = Message<"saas.accounts.v1.ModuleTenantIdentity"> & { + /** + * principal_id is the module's own service principal, which owns the + * capability and acts directly — there is no person and no actor hop. + * + * @generated from field: string principal_id = 1; + */ + principalId: string; + + /** + * tenant is the organization it would act for, and audience the service the + * binding declares it may call. A binding whose audience is the module's own + * prefix is one the module admits itself, which is how unattended work is + * admitted by the module that executes it. + * + * @generated from field: string tenant = 2; + */ + tenant: string; + + /** + * @generated from field: string audience = 3; + */ + audience: string; + + /** + * scopes is exactly what the capability would carry, as + * `kind:action[:resource]`: the binding's headless_scopes narrowed by THIS + * organization's installation scope. Never its invoke_scopes, which describe + * authority exercised on a person's behalf. + * + * @generated from field: repeated string scopes = 4; + */ + scopes: string[]; + + /** + * lifetime_seconds is the fixed lifetime every such capability carries. + * + * @generated from field: int64 lifetime_seconds = 5; + */ + lifetimeSeconds: bigint; + + /** + * revision is the authorization revision it would be sealed with: a digest + * of the module's whole effective entry, so any change to the entry stops + * confirming every capability minted under the old one. + * + * @generated from field: uint64 revision = 6; + */ + revision: bigint; + + /** + * shared says the principal is unpinned and declares cross_tenant, so it may + * act for more than one organization. A pinned principal reads false and can + * only ever name the tenant its envelope declares. + * + * @generated from field: bool shared = 7; + */ + shared: boolean; +}; + +/** + * Describes the message saas.accounts.v1.ModuleTenantIdentity. + * Use `create(ModuleTenantIdentitySchema)` to create a new message. + */ +export const ModuleTenantIdentitySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 53); + +/** + * ModuleTenantIdentityMint is one capability actually issued, as the audit + * trail recorded it. There is no other record: a capability lives a minute and + * nothing caches one. + * + * @generated from message saas.accounts.v1.ModuleTenantIdentityMint + */ +export type ModuleTenantIdentityMint = Message<"saas.accounts.v1.ModuleTenantIdentityMint"> & { + /** + * @generated from field: string record = 1; + */ + record: string; + + /** + * @generated from field: string tenant = 2; + */ + tenant: string; + + /** + * @generated from field: string binding = 3; + */ + binding: string; + + /** + * @generated from field: string audience = 4; + */ + audience: string; + + /** + * @generated from field: repeated string scopes = 5; + */ + scopes: string[]; + + /** + * @generated from field: google.protobuf.Timestamp minted_at = 6; + */ + mintedAt?: Timestamp; +}; + +/** + * Describes the message saas.accounts.v1.ModuleTenantIdentityMint. + * Use `create(ModuleTenantIdentityMintSchema)` to create a new message. + */ +export const ModuleTenantIdentityMintSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 54); + +/** + * @generated from message saas.accounts.v1.ResolveModuleTenantIdentityResponse + */ +export type ResolveModuleTenantIdentityResponse = Message<"saas.accounts.v1.ResolveModuleTenantIdentityResponse"> & { + /** + * identity is set when a mint for this organization would be issued; refusal + * says why it would not. Exactly one is present, and a refusal is a decision + * about declared authority rather than an outage. + * + * @generated from field: saas.accounts.v1.ModuleTenantIdentity identity = 1; + */ + identity?: ModuleTenantIdentity; + + /** + * @generated from field: string refusal = 2; + */ + refusal: string; + + /** + * recent is the trail of capabilities this module was actually issued for + * this organization, newest first. + * + * @generated from field: repeated saas.accounts.v1.ModuleTenantIdentityMint recent = 3; + */ + recent: ModuleTenantIdentityMint[]; +}; + +/** + * Describes the message saas.accounts.v1.ResolveModuleTenantIdentityResponse. + * Use `create(ResolveModuleTenantIdentityResponseSchema)` to create a new message. + */ +export const ResolveModuleTenantIdentityResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 55); + /** * CatalogueEntryKind distinguishes a composed module from a solution. * @@ -2018,6 +2203,18 @@ export const PlatformAdminService: GenService<{ input: typeof PutModuleEnvelopeRequestSchema; output: typeof PutModuleEnvelopeResponseSchema; }, + /** + * ResolveModuleTenantIdentity answers what a per-tenant module operation + * context would assert for one module, one of its bindings and one + * organization — or why it would be refused. It issues no capability. + * + * @generated from rpc saas.accounts.v1.PlatformAdminService.ResolveModuleTenantIdentity + */ + resolveModuleTenantIdentity: { + methodKind: "unary"; + input: typeof ResolveModuleTenantIdentityRequestSchema; + output: typeof ResolveModuleTenantIdentityResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_saas_accounts_v1_platform_admin, 0); diff --git a/module/services/frontend/code/src/gen/saas/frontend/v1/plugin_catalog.ts b/module/services/frontend/code/src/gen/saas/frontend/v1/plugin_catalog.ts index edc8aac60..14373dfa0 100644 --- a/module/services/frontend/code/src/gen/saas/frontend/v1/plugin_catalog.ts +++ b/module/services/frontend/code/src/gen/saas/frontend/v1/plugin_catalog.ts @@ -59,6 +59,7 @@ export const FRONTEND_ROUTES = [ { path: "/admin/platform/events", sourcePath: "src/app/admin/platform/events/page.tsx", match: "exact", access: "super_admin" }, { path: "/admin/platform/feature-flags", sourcePath: "src/app/admin/platform/feature-flags/page.tsx", match: "exact", access: "super_admin" }, { path: "/admin/platform/jobs", sourcePath: "src/app/admin/platform/jobs/page.tsx", match: "exact", access: "super_admin" }, + { path: "/admin/platform/module-identity", sourcePath: "src/app/admin/platform/module-identity/page.tsx", match: "exact", access: "super_admin" }, { path: "/admin/platform/organizations", sourcePath: "src/app/admin/platform/organizations/page.tsx", match: "exact", access: "super_admin" }, { path: "/admin/platform/plugin-transport", sourcePath: "src/app/admin/platform/plugin-transport/page.tsx", match: "exact", access: "super_admin" }, { path: "/admin/platform/waitlist", sourcePath: "src/app/admin/platform/waitlist/page.tsx", match: "exact", access: "super_admin" }, diff --git a/module/services/frontend/code/ui-inventory.json b/module/services/frontend/code/ui-inventory.json index ed18d50b9..1ca56615c 100644 --- a/module/services/frontend/code/ui-inventory.json +++ b/module/services/frontend/code/ui-inventory.json @@ -2510,6 +2510,37 @@ "coverage": "missing", "remaining": "Owner stories or a reviewed nonvisual exclusion still required." }, + { + "source": "src/app/admin/platform/module-identity/page.tsx", + "imports": [ + "@/components/auth/role-gate", + "@/features/module-tenant-identity" + ], + "owner": "SaaS application", + "disposition": "keep feature-local", + "reason": "Next.js route or layout entry; owns host routing and server/provider composition. Full-page preview coverage is still pending.", + "stories": [], + "coverage": "missing", + "remaining": "Owner stories or a reviewed nonvisual exclusion still required." + }, + { + "source": "src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx", + "imports": [ + "@bufbuild/protobuf/wkt", + "lucide-react", + "react", + "@/components/empty-state", + "@/shared/lib/read-outcome", + "@/shared/ui", + "../service/queries" + ], + "owner": "SaaS application", + "disposition": "keep feature-local", + "reason": "Platform playground for the per-tenant module identity: it composes the kit's card, table and badge primitives and owns no visual of its own.", + "stories": [], + "coverage": "missing", + "remaining": "Owner stories or a reviewed nonvisual exclusion still required." + }, { "source": "src/app/admin/platform/waitlist/page.tsx", "imports": [ diff --git a/module/services/frontend/generated/plugin-catalog.json b/module/services/frontend/generated/plugin-catalog.json index 9f9ade780..5fd5967a8 100644 --- a/module/services/frontend/generated/plugin-catalog.json +++ b/module/services/frontend/generated/plugin-catalog.json @@ -125,6 +125,12 @@ "match": "FRONTEND_ROUTE_MATCH_EXACT", "access": "FRONTEND_ROUTE_ACCESS_SUPER_ADMIN" }, + { + "path": "/admin/platform/module-identity", + "source_path": "src/app/admin/platform/module-identity/page.tsx", + "match": "FRONTEND_ROUTE_MATCH_EXACT", + "access": "FRONTEND_ROUTE_ACCESS_SUPER_ADMIN" + }, { "path": "/admin/platform/organizations", "source_path": "src/app/admin/platform/organizations/page.tsx", From 03c5bb6f7679ac4eb63a7faa3052fd44487c0b3e Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:32:58 -0400 Subject: [PATCH 04/20] test(accounts): pin the moved page's derived access boundary in the route-set fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The route SET is asserted rather than its size, so a page that moves changes two lines here — which is the point: a shared count would merge silently. Co-Authored-By: Claude Opus 5 (1M context) --- .../accounts/code/pkg/cataloggen/frontend_plugins_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go b/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go index 6c89a77bf..b3bc74afb 100644 --- a/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go +++ b/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go @@ -69,8 +69,8 @@ func TestFrontendPageDiscoveryPinsAccessAndMatch(t *testing.T) { "/admin/platform/feature-flags", "/admin/platform/jobs", "/admin/platform/organizations", + "/admin/platform/module-identity", "/admin/platform/plugin-transport", - "/admin/platform/unattended-work", "/admin/platform/waitlist", "/admin/roles", "/admin/roles/{roleId}", @@ -79,6 +79,10 @@ func TestFrontendPageDiscoveryPinsAccessAndMatch(t *testing.T) { "/admin/sso", "/admin/teams", "/admin/teams/{teamId}", + // Under /admin/, so the access boundary the generator derives is `admin` + // — the same one the page's own gate declares (issue #1121). Under + // /admin/platform/ it derived `super_admin` and the two disagreed. + "/admin/unattended-work", "/admin/users", "/admin/users/{userId}", "/admin/webhooks", From 39ac7748cbba1de9ceeee144b1aef5ecdb4fc637 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:41:08 -0400 Subject: [PATCH 05/20] chore(frontend): move the two kit packages whose published bytes changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@codefly-dev/saas-plugin-react` 0.4.6 and the co-versioned `@codefly-dev/ui` / `@codefly-dev/saas-ui` 0.12.12. Only their workspace pin on `@codefly-dev/saas-plugin-contract` moved (2.4.4 → 2.5.0), but package.json ships in the tarball, so the bytes a consumer resolves are different — and a registry version is immutable, so publishing changed bytes under the served version is refused at release time and consumers keep resolving the old ones until then. A PATCH on each: the kit is 0.x, where a minor takes every remote's `^0.N` range off the version it pins. Co-Authored-By: Claude Opus 5 (1M context) --- module/services/frontend/code/package-lock.json | 10 +++++----- module/services/frontend/code/package.json | 2 +- .../frontend/code/packages/codefly-ui/package.json | 4 ++-- .../code/packages/saas-plugin-react/package.json | 2 +- .../frontend/code/packages/saas-ui/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/module/services/frontend/code/package-lock.json b/module/services/frontend/code/package-lock.json index f83db0cf8..3a8b1ecd5 100644 --- a/module/services/frontend/code/package-lock.json +++ b/module/services/frontend/code/package-lock.json @@ -14,7 +14,7 @@ "@base-ui/react": "^1.3.0", "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-plugin-contract": "2.5.0", - "@codefly-dev/saas-plugin-react": "0.4.5", + "@codefly-dev/saas-plugin-react": "0.4.6", "@codefly-dev/saas-sdk": "0.3.30", "@codefly-dev/saas-ui": "0.12.8", "@codefly-dev/ui": "0.12.8", @@ -15360,7 +15360,7 @@ }, "packages/codefly-ui": { "name": "@codefly-dev/ui", - "version": "0.12.11", + "version": "0.12.12", "license": "Apache-2.0", "dependencies": { "@base-ui/react": "^1.3.0", @@ -15383,7 +15383,7 @@ }, "devDependencies": { "@codefly-dev/saas-plugin-contract": "2.5.0", - "@codefly-dev/saas-plugin-react": "0.4.5", + "@codefly-dev/saas-plugin-react": "0.4.6", "@tailwindcss/postcss": "^4", "@testing-library/react": "^16.1.0", "@types/hast": "^3.0.4", @@ -15456,7 +15456,7 @@ }, "packages/saas-plugin-react": { "name": "@codefly-dev/saas-plugin-react", - "version": "0.4.5", + "version": "0.4.6", "license": "Apache-2.0", "devDependencies": { "@codefly-dev/saas-plugin-contract": "2.5.0", @@ -15508,7 +15508,7 @@ }, "packages/saas-ui": { "name": "@codefly-dev/saas-ui", - "version": "0.12.11", + "version": "0.12.12", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "1.9.1", diff --git a/module/services/frontend/code/package.json b/module/services/frontend/code/package.json index a780ed293..8a65060ee 100644 --- a/module/services/frontend/code/package.json +++ b/module/services/frontend/code/package.json @@ -38,7 +38,7 @@ "@base-ui/react": "^1.3.0", "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-plugin-contract": "2.5.0", - "@codefly-dev/saas-plugin-react": "0.4.5", + "@codefly-dev/saas-plugin-react": "0.4.6", "@codefly-dev/saas-sdk": "0.3.30", "@codefly-dev/saas-ui": "0.12.8", "@codefly-dev/ui": "0.12.8", diff --git a/module/services/frontend/code/packages/codefly-ui/package.json b/module/services/frontend/code/packages/codefly-ui/package.json index 7f9be3b10..92faa8012 100644 --- a/module/services/frontend/code/packages/codefly-ui/package.json +++ b/module/services/frontend/code/packages/codefly-ui/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/ui", - "version": "0.12.11", + "version": "0.12.12", "description": "The shared, versioned Codefly SaaS frontend kit: the plugin host and the tokens-as-data skin mechanism, standalone so the host and Module-Federation remotes import and dedupe one copy.", "repository": { "type": "git", @@ -117,7 +117,7 @@ }, "devDependencies": { "@codefly-dev/saas-plugin-contract": "2.5.0", - "@codefly-dev/saas-plugin-react": "0.4.5", + "@codefly-dev/saas-plugin-react": "0.4.6", "@testing-library/react": "^16.1.0", "@tailwindcss/postcss": "^4", "@types/react": "^19.2.17", diff --git a/module/services/frontend/code/packages/saas-plugin-react/package.json b/module/services/frontend/code/packages/saas-plugin-react/package.json index 783676335..9333c49b6 100644 --- a/module/services/frontend/code/packages/saas-plugin-react/package.json +++ b/module/services/frontend/code/packages/saas-plugin-react/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/saas-plugin-react", - "version": "0.4.5", + "version": "0.4.6", "description": "Product-neutral React contribution composition and runtime adapters for the Codefly SaaS frontend plugin host.", "type": "module", "sideEffects": false, diff --git a/module/services/frontend/code/packages/saas-ui/package.json b/module/services/frontend/code/packages/saas-ui/package.json index 0c5c5aa64..2d1119314 100644 --- a/module/services/frontend/code/packages/saas-ui/package.json +++ b/module/services/frontend/code/packages/saas-ui/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/saas-ui", - "version": "0.12.11", + "version": "0.12.12", "description": "Reusable SaaS-domain frontend components (datasources, …) the portal and solutions both import, built on @codefly-dev/ui primitives and driven by an injected typed client.", "repository": { "type": "git", From 52cde7c5bce42fe6877e713efcff337c50f41b95 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:44:30 -0400 Subject: [PATCH 06/20] fix(frontend): move the reference composition's kit pins with the packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `module/contracts/reference/frontend` is the composition a consumer is built against in `test-published-plugin-contract`, and its dependencies are EXACT pins. Left at 2.4.4 / 0.4.5 they no longer name a packed tarball, so npm went to the public registry and 404'd — the gate failing on a version that does not exist rather than on anything it is meant to catch. Co-Authored-By: Claude Opus 5 (1M context) --- module/contracts/reference/frontend/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/contracts/reference/frontend/package.json b/module/contracts/reference/frontend/package.json index bc1de1537..acea1cb9f 100644 --- a/module/contracts/reference/frontend/package.json +++ b/module/contracts/reference/frontend/package.json @@ -18,8 +18,8 @@ "build": "tsc -p tsconfig.json" }, "dependencies": { - "@codefly-dev/saas-plugin-contract": "2.4.4", - "@codefly-dev/saas-plugin-react": "0.4.5" + "@codefly-dev/saas-plugin-contract": "2.5.0", + "@codefly-dev/saas-plugin-react": "0.4.6" }, "peerDependencies": { "react": ">=19.2 <20" From eafdbd48abc4b4a0607b55c9d0ed84a1c91b1c70 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:14:37 -0400 Subject: [PATCH 07/20] style(accounts): goimports' alignment on the struct the sweep switch joined `gofmt -l` was clean and the quality gate still refused: goimports re-aligns a struct's trailing comments across the whole run, and the new field's longer name moved the column for every field after it. The gate is goimports, not gofmt. Co-Authored-By: Claude Opus 5 (1M context) --- .../accounts/code/pkg/business/service.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/module/services/accounts/code/pkg/business/service.go b/module/services/accounts/code/pkg/business/service.go index 2026c9615..58cd0ee92 100644 --- a/module/services/accounts/code/pkg/business/service.go +++ b/module/services/accounts/code/pkg/business/service.go @@ -93,18 +93,18 @@ type Service struct { githubAppClientID string // the App's OAuth client, which identifies the person returning from an install githubAppClientSecret string // its secret; without the pair, an installation cannot be attributed to a caller //nolint:lll // the mount this mirrors is a single deployment switch; splitting the note loses the pairing - datasourceWebhookMounted bool // whether THIS deployment mounted the per-source push receiver; a stored per-source secret verifies nothing without it + datasourceWebhookMounted bool // whether THIS deployment mounted the per-source push receiver; a stored per-source secret verifies nothing without it //nolint:lll // one deployment switch; the stopgap it disables is named in datasource_deliveries.go datasourceReconcileSweepOff bool // STOPGAP switch (#1121): true stops this host firing the reconcile schedule it DECLARES datasourceTicketSigner *datasourceTicketSigner // mints/verifies opaque content tickets for oversized change-set blobs - newGitHubClient func(token string) GitHubContentClient - newAPIClient func(cfg APIDatasourceConfig, credential string) APIContentClient - newCrawlerClient func(cfg CrawlerDatasourceConfig) CrawlerContentClient - newUploadClient func(cfg UploadDatasourceConfig, secretAccessKey string) UploadContentClient - newOAuth2Refresh OAuth2RefreshFunc // refreshes an OAuth 2.0 API source's access token - moduleProducer jobs.Producer // request-scoped, transactional outbox producer for the module-facing surface - moduleJobStore jobs.Store // privileged worker store (claim/finalize) for the module-facing surface - eventTransport events.Transport // domain-event pub/sub transport (transactional outbox + relay); nil denies publish/replay + newGitHubClient func(token string) GitHubContentClient + newAPIClient func(cfg APIDatasourceConfig, credential string) APIContentClient + newCrawlerClient func(cfg CrawlerDatasourceConfig) CrawlerContentClient + newUploadClient func(cfg UploadDatasourceConfig, secretAccessKey string) UploadContentClient + newOAuth2Refresh OAuth2RefreshFunc // refreshes an OAuth 2.0 API source's access token + moduleProducer jobs.Producer // request-scoped, transactional outbox producer for the module-facing surface + moduleJobStore jobs.Store // privileged worker store (claim/finalize) for the module-facing surface + eventTransport events.Transport // domain-event pub/sub transport (transactional outbox + relay); nil denies publish/replay // Outbound API operation and OAuth provider dependencies. newOAuth2AuthorizationCode func(context.Context, apisource.OAuth2Config, string, string, string, string) (*apisource.OAuth2Token, error) From 5c4ef93ce67655b78430407c470fe277623eb9c3 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:20:37 -0400 Subject: [PATCH 08/20] fix(frontend,docs): the identity page obeys the read-outcome rules, and the SDK-gap diagnosis is withdrawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An independent review of #1122's head (52cde7c5b) found two required corrections. Both are real; the second was my error. H1 — the "Capabilities issued" card answered without evidence. It rendered straight from `answer.recent`, applying neither the outcome nor the wait that the card above it applies, and the query kept its data when authority was refused. Two concrete wrong answers: before any read had run, the page stated "No capability has been issued to this module for this organization" — a factual claim with nothing behind it; and after a refetch came back 403, the previously read audit row stayed on screen. `module/services/frontend/AGENTS.md` § "Two rules every admin surface owes its reader" is explicit about both, and about where the fix goes: a refused read deletes the retained snapshots for every cached key IN THE QUERY FUNCTION, because authority is a property of the read and not of the component that happens to be mounted. Both cards now gate on `useLoadingPhase` plus the read outcome, a forbidden read clears the cached identity keys, and a transient failure keeps the prior answer under a stale notice. Four tests, two of which go red when the query-side clearing is reverted — including the case where a forbidden read is followed by a transient one, which must not bring the snapshot back. H2 — the claim that the published module-authority client "takes no tenant" was WRONG, and it pointed tenant integration at the wrong owner. `saas-sdk-go` v0.1.0 has `Credentials.OrganizationID` (moduleauthority/client.go:131) and sends it as `organization_id` on both mints (broker.go:42,48; client.go:356,381); v0.1.1 and v0.1.2 carry the same facility. Verified in the module cache against each. WORK_CONTEXTS.md, MODULE.md, accounts/AGENTS.md and CLAIM_INVENTORY.md now describe what the client actually offers — one client per organization, because it caches the module Work Context belonging to that organization, and outbound calls go through `MintModuleOperationContext` with the installed binding, since a module Work Context addresses the host's own capability surface and is not authority to call another audience. What replaces the false gap is a narrower, true one: this repository's `qualification/module-authority` still pins a pre-0.1.0 pseudo-version whose mints take no organization, so the per-tenant path is proved at this host's seam and not through the published client. Raising that pin with a two-organization qualification case is the work that remains, and it is named rather than done here. The H1 fix and the doc corrections were drafted by the reviewing session in this worktree; I reviewed them, verified the SDK claim myself, added the qualification-pin fact the drafts omitted, and fixed the import ordering biome refused. Co-Authored-By: Claude Opus 5 (1M context) --- CLAIM_INVENTORY.md | 2 +- MODULE.md | 4 +- module/WORK_CONTEXTS.md | 30 +++++-- module/services/accounts/AGENTS.md | 5 +- .../module-tenant-identity/service/queries.ts | 24 +++++- ...ant-identity-playground.container.test.tsx | 83 ++++++++++++++++++- .../ui/module-tenant-identity-playground.tsx | 67 +++++++++++---- 7 files changed, 184 insertions(+), 31 deletions(-) diff --git a/CLAIM_INVENTORY.md b/CLAIM_INVENTORY.md index 7f3c4b1dd..0d7f1aa64 100644 --- a/CLAIM_INVENTORY.md +++ b/CLAIM_INVENTORY.md @@ -116,7 +116,7 @@ must not be inferred from a row here. See | Every pass a source ever took, listed | — | none, and none is possible here: a receipt is recovered by identity and never enumerated, so the panel shows only the passes this browser admitted and says so | not claimed | accounts | | A module identity minted for ONE tenant, bounded by that tenant's own installation | accounts/AGENTS.md § A module identity minted for one tenant; WORK_CONTEXTS.md § A module identity minted for one tenant | `TestSharedInstallationMintSelectsOnlyConsentingOrganization` (`pkg/business`) mints for two organizations and compares what each capability carries; `TestResolveModuleTenantIdentityAnswersWhatOneTenantsMintWouldAssert` and `TestResolveModuleTenantIdentityRefusesATenantThePinForbids` hold the operator read to the same four answers the mint gives | implemented | accounts | | An operator can read what a module's per-tenant capability would carry, without one being issued | accounts/AGENTS.md § A module identity minted for one tenant | `module-tenant-identity-playground.container.test.tsx` records every request the page makes and asserts it is the resolution alone; the authorization table pins `ResolveModuleTenantIdentity` at `super_admin` | implemented | accounts, frontend | -| A module reaching the per-tenant mint through the client it is given | WORK_CONTEXTS.md § A module identity minted for one tenant | none: `saas-sdk-go/moduleauthority`'s `ModuleWorkContext(ctx)` and `MintModuleOperationContext(ctx, binding)` take no tenant, so a module consuming that client mints one identity for the whole deployment. The host accepts the organization on both mints and the gateway relays it | planned | the module-authority client | +| A module selecting the per-tenant mint through its published client | WORK_CONTEXTS.md § A module identity minted for one tenant | `saas-sdk-go/moduleauthority.Credentials.OrganizationID` is present in v0.1.0 (and v0.1.1/v0.1.2); each client fixes one organization and both mint request bodies carry it. NOT exercised from this repository: `qualification/module-authority` pins `v0.0.3-0.20261011073356-ac003d53e6b1`, whose mints take no organization, so the per-tenant path is proved at this host's own seam. A composed caller must still supply the tenant and the installed operation binding | implemented (client) / unexercised here | module-authority client; this repo's qualification pin; composed consumer proof remains open | | A DECLARED schedule actually coming due, in a composed environment | MODULE.md § Unattended work ("what is not proven") | none — nothing in this repository composes a module that executes declared work, and the module that fires triggers keys one installed trigger per release name while this host declares every source under one | planned | #1081 | ## Handbook and interface surfaces diff --git a/MODULE.md b/MODULE.md index 000dbc134..749d8dfe3 100644 --- a/MODULE.md +++ b/MODULE.md @@ -265,8 +265,8 @@ not per deployment. That is what a consumer admitting a publication for a tenant reads the publisher's tenant from. `PlatformAdminService/ResolveModuleTenantIdentity` and `/admin/platform/module-identity` answer what one mint would assert, or why it -would be refused, and issue nothing; the client-side gap that still stops a -module reaching it (`saas-sdk-go/moduleauthority` takes no tenant) is named in +would be refused, and issue nothing. The existing client selects one tenant +through `Credentials.OrganizationID`; its operation-mint wiring is described in [WORK_CONTEXTS.md](./module/WORK_CONTEXTS.md#a-module-identity-minted-for-one-tenant). ## The host as an OAuth 2.1 authorization server diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index c71dd22bd..22cdc70b6 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -581,14 +581,28 @@ surface that answered an operator with a module's capability would be a second spelling of that module's identity. So the read resolves the decision from the same declared facts and stops before the signature. -**What is still owed elsewhere.** The host accepts the organization on both -mints and the gateway relays it, but the published module-authority client's -`ModuleWorkContext(ctx)` and `MintModuleOperationContext(ctx, binding)` take no -tenant — so a module consuming that client mints one identity for the whole -deployment and cannot name an organization at all. That is a client-side gap in -`saas-sdk-go/moduleauthority`, not here, and until it closes a module cannot -reach this capability through the client it is given. Nothing in this repository -works around it. +**Selecting a tenant through the published client.** +`saas-sdk-go/moduleauthority` already exposes `Credentials.OrganizationID` +(including v0.1.0). Construct a separate client for each organization: both +`ModuleWorkContext` and `MintModuleOperationContext` send that selector. For an +outbound operation use `MintModuleOperationContext` with the installed binding; +`ModuleWorkContext` addresses the host's capability broker and is not authority +to call a different audience. The operation result names its tenant and +principal; a publisher must hold that tenant to its committed change and use +that principal as its source. This is consumer wiring, not a missing mint. + +**What this repository does not exercise.** `qualification/module-authority` +still pins `v0.0.3-0.20261011073356-ac003d53e6b1`, a pre-0.1.0 pseudo-version +whose mints take no organization at all — so the per-tenant mint is proved +against this host's own seam and not through the published client. Raising that +pin, with a qualification case that mints for two organizations through it, is +the gap that remains here. + +The operator page and its issued-capability trail distinguish a read not yet +requested, loading, a failed read and a successful empty result. A forbidden +read clears retained identity snapshots, so later refresh failures cannot +redisplay authority the viewer was refused. Transient failures retain a prior +successful answer with a stale-data notice. ## Operation contexts from a source delegation diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index ce7e2e240..0a5db40ea 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -1065,8 +1065,9 @@ browser has none, and a surface that answered an operator with a module's capability would be a second spelling of that module's identity. The playground is `/admin/platform/module-identity`. -The client-side gap that still stops a module reaching this — -`saas-sdk-go/moduleauthority` takes no tenant on either mint — is named in +The published client selects the tenant through `Credentials.OrganizationID`, +using a separate client per organization. Outbound calls use the installed +operation binding through `MintModuleOperationContext`, as described in [WORK_CONTEXTS.md](../WORK_CONTEXTS.md#a-module-identity-minted-for-one-tenant). ## Subject visibility is a projection, not a module's own vocabulary diff --git a/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts index c1dfef14d..9d0664351 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts +++ b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts @@ -1,5 +1,6 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { usePlatformAdminService } from "@/lib/hooks/use-api-client"; +import { mayKeepRetainedRows, readOutcome } from "@/shared/lib/read-outcome"; /** * The read behind the per-tenant module identity playground. @@ -20,10 +21,27 @@ export function useModuleTenantIdentity( organizationId: string, ) { const service = usePlatformAdminService(); + const client = useQueryClient(); return useQuery({ queryKey: ["module-tenant-identity", prefix, binding, organizationId], enabled: !!prefix && !!binding && !!organizationId, - queryFn: () => - service.resolveModuleTenantIdentity({ prefix, binding, organizationId }), + queryFn: async () => { + try { + return await service.resolveModuleTenantIdentity({ + prefix, + binding, + organizationId, + }); + } catch (error) { + if (!mayKeepRetainedRows(readOutcome(true, error))) { + for (const query of client + .getQueryCache() + .findAll({ queryKey: ["module-tenant-identity"] })) { + query.setState({ data: undefined, dataUpdatedAt: 0 }); + } + } + throw error; + } + }, }); } diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx index d07b194fb..2bd868eec 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx @@ -122,7 +122,9 @@ describe("ModuleTenantIdentityPlayground", () => { // The trail of capabilities actually issued, which is the only record of // one: nothing caches a capability. - expect(document.querySelectorAll('[data-slot="module-tenant-mint"]')).toHaveLength(1); + expect( + document.querySelectorAll('[data-slot="module-tenant-mint"]'), + ).toHaveLength(1); // And the whole point: one request, the resolution, carrying the triple // — and no mint anywhere. @@ -154,3 +156,82 @@ describe("ModuleTenantIdentityPlayground", () => { ).toBeNull(); }); }); + +describe("independent review: authority read outcomes", () => { + it("does not claim no issued capabilities before a read", () => { + renderInApp(); + expect(screen.queryByText(/No capability has been issued/)).toBeNull(); + }); + it("removes issued capability rows after a forbidden refetch", async () => { + serve(minted); + renderInApp(); + fill(); + await screen.findByText("audit-1"); + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + () => + HttpResponse.json( + { + code: "permission_denied", + message: "requires platform super_admin role", + }, + { status: 403 }, + ), + ), + ); + fireEvent.click(screen.getByRole("button", { name: "Resolve again" })); + await screen.findAllByText(/not allowed|permission|cannot view|access/i); + expect(screen.queryByText("audit-1")).toBeNull(); + }); + it("does not restore a forbidden snapshot after a later unavailable read", async () => { + serve(minted); + renderInApp(); + fill(); + await screen.findByText("audit-1"); + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + () => + HttpResponse.json( + { code: "permission_denied", message: "refused" }, + { status: 403 }, + ), + ), + ); + fireEvent.click(screen.getByRole("button", { name: "Resolve again" })); + await screen.findAllByText(/don't have permission/); + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + () => + HttpResponse.json( + { code: "unavailable", message: "unavailable" }, + { status: 503 }, + ), + ), + ); + fireEvent.click(screen.getByRole("button", { name: "Resolve again" })); + await screen.findAllByText(/Couldn't load/); + expect(screen.queryByText("audit-1")).toBeNull(); + }); + it("retains a successful answer with a notice on transient failure", async () => { + serve(minted); + renderInApp(); + fill(); + await screen.findByText("audit-1"); + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + () => + HttpResponse.json( + { code: "unavailable", message: "unavailable" }, + { status: 503 }, + ), + ), + ); + fireEvent.click(screen.getByRole("button", { name: "Resolve again" })); + await screen.findByText(/may be out of date/); + expect(screen.getByText("audit-1")).toBeTruthy(); + }); +}); diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx index aab892d13..5b8907eab 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx @@ -7,6 +7,7 @@ import { EmptyState } from "@/components/empty-state"; import { readOutcome, readOutcomeMessage, + staleReadNotice, } from "@/shared/lib/read-outcome"; import { Badge, @@ -25,6 +26,7 @@ import { TableHead, TableHeader, TableRow, + useLoadingPhase, } from "@/shared/ui"; import { useModuleTenantIdentity } from "../service/queries"; @@ -73,7 +75,14 @@ export function ModuleTenantIdentityPlayground() { // A disabled query is not a loading one: with a field empty it sits at // isPending with an idle fetch status, and a wait for a read that was never // issued is a spinner that never resolves. - const waiting = read.isPending && read.fetchStatus !== "idle"; + const phase = useLoadingPhase(read.isFetching); + const waiting = phase.indicator || phase.quiet; + const stale = answer + ? staleReadNotice( + outcome, + "this module’s authority and issued capabilities", + ) + : null; return (
@@ -140,11 +149,22 @@ export function ModuleTenantIdentityPlayground() {
- {waiting ? ( - - ) : outcome !== "empty" ? ( + + {stale && (

- {readOutcomeMessage(outcome, "this module's declared authority", "")} + {stale} +

+ )} + {waiting ? null : outcome !== "empty" && !answer ? ( +

+ {readOutcomeMessage( + outcome, + "this module's declared authority", + "", + )}

) : !answer ? ( Every mint is recorded, and the record is the only trace: a capability lives about a minute and nothing caches one. A reference - that no longer resolves shows up as the next mint having no row - here at all. + that no longer resolves shows up as the next mint having no row here + at all. - {!answer || answer.recent.length === 0 ? ( + + {waiting ? null : !answer ? ( +

+ {readOutcomeMessage( + outcome, + "issued capabilities", + "Complete the selection above to read issued capabilities.", + )} +

+ ) : answer.recent.length === 0 ? (

- + Scopes, as this organization's installation narrowed them

-

+

{identity.scopes.join(", ") || "—"}

The owner is the module's own service principal and it acts - directly: no person, and no actor hop. The scopes are the - binding's headless_scopes narrowed by this - organization's installation scope, never the{" "} - invoke_scopes it holds for work a person asked for. + directly: no person, and no actor hop. The scopes are the binding's{" "} + headless_scopes narrowed by this organization's + installation scope, never the invoke_scopes it holds for + work a person asked for.

); From 2cda10b86cd649298ccdcc440cc81739caa8a546 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:55:34 -0400 Subject: [PATCH 09/20] =?UTF-8?q?fix(accounts,frontend):=20a=20self-addres?= =?UTF-8?q?sed=20delegation=20is=20a=20candidate,=20not=20the=20executor?= =?UTF-8?q?=20=E2=80=94=20and=20the=20sweep's=20state=20reaches=20the=20wi?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An independent review of 5c4ef93ce found F1: the declaration inferred "the executing module holds a usable delegation" from the weaker fact that SOME module holds a self-addressed one. The counterexample is real. Source S has a live self-addressed delegation held by module A; the composed executor is B, which holds none; A satisfied the predicate, the declaration emitted S, and the page said "a delegation reference the executing module holds". B's mint then resolves ActiveSourceDelegation(S, B) and is correctly refused (ErrSourceDelegationMissing) — so the mint was right and this host was telling an operator a source was ready when every occurrence would fail. I checked whether the stronger resolution is available and it is not: nothing in the envelope or the derived registry declares which module executes declared work. Self-addressed bindings are generic and a deployment may have several, and there is no executor selector on the declaration read. Inventing one, or hard-coding a sibling module's prefix, is not a contract — so the projection is made honest instead: - `usableByExecutor` → `candidateForADeclaredExecutor`, whose comment states what it establishes (the one shape a source-id reference can serve, because a mint by source id resolves the CALLING module's delegation) and what it does not (who executes). - the declaration reports the HOLDER — `IdentityHolder`, `identity_holder` on the wire — which the arm itself cannot carry, its value being the source id. That is the fact that made A and B indistinguishable. - the page: "A delegation reference held by — whether it executes this release is unverified". - CLAIM_INVENTORY gains a **not claimed** row: executor readiness, "none, and none is possible from here", needing a contract that names the executing principal. Regressions vary the holder independently, which the first version of the test did not vary at all: A-only and B-only each name the candidate under their own name and produce the SAME `identity.source`; revoking B's delegation or moving its binding digest drops the identity and the holder rather than falling back to A. And at the real mint route, A mints from the source id while B presents the same reference and is refused with no child recorded and A's row untouched. SECOND BUG, found while tracing F1 through the projection and not named by the review: `triggerDeclarationToProto` never carried `HostSweepEnabled` or `HostSweepDueAt` at all. The fields existed, the panel was written against them, and the wire dropped both — so the page would have told every operator this host fires nothing, the exact inverse of what that change was made to say. Nothing caught it because the panel's container test serves its own JSON fixture and never crosses this projection. `TestTriggerDeclarationProjectionCarriesEveryField` now asserts every field by name, plus the undeclarable-source and sweep-off cases, because a projection is where a value is dropped in silence. Also: the claim that the sweep's tick "is not installed" was stronger than `work.go` shows. The ticker still fires and its callback still runs; the refusal is inside `RunDatasourceReconcile`, which is where the guard belongs — a switch read only at the ticker would leave every other caller unguarded. Corrected in the three places that said otherwise. And the deterministic CI red is fixed: `frontend-plugin-public-api.json` still declared `packageVersion` 0.4.5 for `@codefly-dev/saas-plugin-react` after the package moved to 0.4.6, which is the one frontend test that failed in CI (1 of 4817). The two saas-sdk generation failures in my local run were my own parallel load — that file spawns `npm run` per test against a 5s default timeout and passes 7/7 alone. Co-Authored-By: Claude Opus 5 (1M context) --- CLAIM_INVENTORY.md | 3 +- module/WORK_CONTEXTS.md | 10 ++ .../api/accounts/authority/contract.binpb | Bin 1185627 -> 1186952 bytes .../proto/saas/accounts/v1/datasource.proto | 39 +++++-- .../api/accounts/connect/contract.binpb | Bin 1185627 -> 1186952 bytes .../proto/saas/accounts/v1/datasource.proto | 39 +++++-- module/contracts/api/catalog.codefly.json | 4 +- module/module.package.codefly.yaml | 4 +- module/services/accounts/AGENTS.md | 38 +++++-- .../code/pkg/adapters/datasource_trigger.go | 9 ++ .../datasource_trigger_identity_test.go | 33 ++++++ .../datasource_trigger_projection_test.go | 103 ++++++++++++++++++ .../pkg/business/datasource_deliveries.go | 10 +- .../code/pkg/business/datasource_trigger.go | 53 +++++++-- .../pkg/business/datasource_trigger_test.go | 77 ++++++++++++- .../pkg/gen/saas/accounts/v1/datasource.pb.go | 55 +++++++--- module/services/accounts/code/work.go | 10 +- .../proto/saas/accounts/v1/datasource.proto | 39 +++++-- .../code/frontend-plugin-public-api.json | 2 +- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1185627 -> 1186952 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../src/gen/saas/accounts/v1/datasource_pb.ts | 46 ++++++-- .../ui/source-trigger-schedules.tsx | 31 +++--- ...ttended-work-playground.container.test.tsx | 18 ++- .../src/gen/saas/accounts/v1/datasource_pb.ts | 46 ++++++-- 26 files changed, 551 insertions(+), 122 deletions(-) create mode 100644 module/services/accounts/code/pkg/adapters/datasource_trigger_projection_test.go diff --git a/CLAIM_INVENTORY.md b/CLAIM_INVENTORY.md index 0d7f1aa64..fe78c597c 100644 --- a/CLAIM_INVENTORY.md +++ b/CLAIM_INVENTORY.md @@ -99,7 +99,8 @@ must not be inferred from a row here. See | One occurrence's enqueue key is scoped by organization and source | accounts/AGENTS.md § A datasource source is refreshed by three things | `TestReconcileOccurrenceKeyIsScopedByOrgAndSource` (`pkg/business`): two organizations' sources under one caller-chosen effect identity produce different keys, each key names all three parts, and two attempts of one occurrence still agree | implemented | accounts | | `DelegateSourceTriggers` is additive and refuses another member's personal source | WORK_CONTEXTS.md § Triggers | `TestDelegateSourceTriggersLeavesAnotherModulesDelegationAlone`, `TestDelegateSourceTriggersRefusesAnotherMembersPersonalSource` (`pkg/business`) assert the delegation rows: another module's survives active and owner-intact, nothing is revoked, and the owner's own call records | implemented | accounts | | The backfill records a reference only for a module that EXECUTES declared work, and replaces the executor's unusable one | accounts/AGENTS.md § Unattended work | `TestDelegateSourceTriggersLeavesAnotherModulesDelegationAlone` asserts a consuming module gets nothing; `TestDelegateSourceTriggersReplacesTheExecutorsStaleDelegation` asserts a stale-digest delegation is revoked as `binding_changed` and replaced while a current one is left untouched. Both red under the matching mutation (dropping the executor filter; always skipping an existing row) | implemented | accounts | -| A declared trigger's reference arm names only a delegation its executor can mint from | accounts/AGENTS.md § Unattended work; WORK_CONTEXTS.md § Triggers | `TestTriggerDeclarationNamesOnlyADelegationItsExecutorCanUse` (`pkg/business`) removes each of the three facts on its own — a delegation addressed to another module, a binding no longer declared, an authority that did not resolve — and asserts the declaration names no identity, plus that an unusable row never shadows a usable one | implemented | accounts | +| A declared trigger's reference arm names only a SELF-ADDRESSED delegation, and reports whose it is | accounts/AGENTS.md § Unattended work; WORK_CONTEXTS.md § Triggers | `TestTriggerDeclarationNamesOnlyADelegationItsExecutorCanUse` (`pkg/business`) removes each of the three facts on its own — addressed to another module, a binding no longer declared, an authority that did not resolve — and asserts no identity is named; `TestTriggerDeclarationNamesTheHolderAndClaimsNoExecutor` varies the HOLDER independently (A-only, B-only, B revoked, B's binding changed) and pins the reported holder; `TestTriggerDeclarationProjectionCarriesEveryField` (`pkg/adapters`) holds every field to the wire | implemented | accounts | +| That the module which EXECUTES a declared release holds the reference | accounts/AGENTS.md § Unattended work; WORK_CONTEXTS.md § Triggers | none, and none is possible from here (#1121, F1): nothing this host holds declares who executes declared work, so a self-addressed delegation identifies a candidate and its holder, never the executor. A composed executor with no delegation on the source is refused at the mint. The read reports the holder and leaves readiness unverified | not claimed | accounts; needs a contract naming the executing principal | | A refused reconcile pass enqueues nothing | accounts/AGENTS.md § A datasource source is refreshed by three things | `TestReconcileSourceOperation_RefusedPassEnqueuesNothing` (`pkg/business`): an effect identity re-presented for another source, and one re-presented with another request digest, are both refused with no job for them anywhere; red when the pre-check is removed | implemented | accounts | | The stopgap sweep can be switched off, and the reader is told which schedule fires and when | accounts/AGENTS.md § Unattended work | `TestTheStopgapSweepCanBeSwitchedOff` (`pkg/business`) asserts the default is ON, that a disabled sweep enqueues nothing and bumps nothing, and that the declaration's `host_sweep_due_at` is the STORED instant and differs from the cron's; `source-trigger-schedules.container.test.tsx` asserts both rows render and that the host's row says "off" with no instant | implemented | accounts, frontend | | `source.synced` says whether anybody was present for the pass | accounts/AGENTS.md § Unattended work | the adapter marks a delegated pass (`business.WithUnattendedPass`) and the observation records `unattended` and the delegation id, declared optional in payload v2. The actor stays the delegating person, which ADR 0008 puts in the trail deliberately | implemented | accounts | diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index 22cdc70b6..b457def3b 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -836,6 +836,16 @@ observe — hence `TestTriggerDeclarationMatchesTheContractFieldNames`, which pins the document's names *and values* and records the other column beside them. +**A declared reference is a candidate, not a readiness claim** (#1121, F1). The +host sets the reference arm for a self-addressed delegation — the only shape a +source-id reference can serve — and reports whose delegation it is +(`identity_holder`). It does **not** establish that the module which executes +this release holds one: nothing the host has declares who that is, those +bindings are generic, and a deployment may have several. A composed executor +with no delegation on the source is refused at the mint +(`ErrSourceDelegationMissing`), so the read names the holder and leaves executor +readiness unverified rather than calling the source bound. + **Exactly one identity, and the two are not alternatives of equal standing.** - `source` — **the grant reference, and the contract.** It names a diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index c7bc3db16ebdf9478699d4def77e8a66997bff99..70814cf1f0777d58cd32b767ba3d27fd4653ca82 100644 GIT binary patch delta 20350 zcmZ{r33OFewytw_b~b0PeR{eOx>Hn@y;zFg=dh}tr4R)bP{8h1d2dB>lw2e6B|*#k z#(NtO#0DXB9uN=#MG*@UKms;^f}+w$qbL@QG%5%vpn{6P`{!KUkujcQWDMrN*P3gs zZ_d^2ef-z_o{uf-Idnv1gfpUeggc^Sgg2tai0ESf%s-qD&NN3J_BUSPZgE9v{(pV$ zmo^`s@28yeGv|tARb_5)eO3L#|2e3px-wT=8e}8q`TY`F?#XKNsYd^gmj$1{Tq0Uz zN{Wi!OXqiIqZc&azt^vQBYiR}9QT@x_hz(cWi(PYvqSTd86xh4{rmPV&-N(qe$zGm z%6nwX?wv!fvfcaMdQ*?=t^La{%04uxYTzJ|rT?nxvJd4d?#tC?>jzZ~&JM1) zziRNk_;)T_GdM?DefFWM>gul9YqEG!pS}CxY+cQe+JU)jRb{p|S65U0V6INOqDmD5 z>+?NkiHloi2Ua{#ad%ZUhKM$}Kz_t5ab=rq?jN~TIg)Gx$w&+@#t*GPfRSc=DqLS#Dd^%{hxFkPqwkRu+_LAA+ z;;_JD{UCT6R8v>akF%Azfz=fl0Jnr;=ei2{;P#%qy7yFllK`sfF2X-l?QCTYDh;lw z&)zq<=Ao{)H@ftuQS1Pr6d!B=(eURi|5TXZ@q+4`FBqHM+BO4WW%t#(sUSv5FU zS4U{!rRwfnZAE>q`e7aws8rpbtIX~hIx$<39awY!18B^;KsMA5LP6=Opby-0IMmru zM?DIMP+jB@^^o#MRy(h}wzfZKQ4ceS-c@yV;CR05aRo<8;vZO1T@5efLOJhpcg>K& zm0hH>hiZmYS7rxQAXV$&G}507I#&&ovKvUR))@H>1(E zA}&hYYn-BzjJrJQL`6)vWU|#tKa7y+C$(8jW~QYwBdEHm0&%Gmeh`gz=5p6^ch+j3Ff%T!jD858YomT#oA9|dno28^ zy0gw!MOj#F^g}j%Qx;X%MO(?vwz<}EPlI^){wl4cIf~>5lL=buM z52|@6ziyHEuIs1Ks3eK1@boE>l%7i9X$N&;sq{o<^Wnwfs>nGzY|l$jX$SRuu7icM zd&b)rEgI%W%ARqu`KBhntohz0A{yzuFBw%~* zBLWqE4@6r_%H16b!6B;jbc8E`!J+)YrDAf|Ls8!pLY+g=c4ev|LVtwnES7bE{z$%Q znOG7Y(V5qs$sf^~*PY29r8<PcP4+#Rt0!ix@#_~9*e3h z`8^7w?lGMv-5p|moa(NYT_rOgye{4ikK3*i>f^Sn9txeX?Rjiv)!03>cOvSy*Cwi- zh<1{5p}W%pojVzIg!rdyt_7PN%bty7>ju_5fHf6s&O@|K^-WR!*E6ML;a5DY7&W!MQ)yN6yqDcrY{W*!)u~MSVEdSGfWaRmS>iLGIyr z2wnrhrCAD-tqWA+l==;Rhu>=xRU7>C&R1p&_@78)UPYHHm zK5cCE`Jr!#1=)$dAy2tJk;rePrLH{VP4kO3H1~a5^o?|$=11k!E9X3zK`l(D7GP)G z8TmI?i5cMx9SMl=HA6?DoD<+V9SN8j7na-&V6mxIMwqDjoUc~Ka)(Ag%Xiv|o4GZ_ zAJ(~L*J36Q%F$7f4=Z#g%KvM%_+!;9zf7*gs0vTB{NKw-RqhnS(`?^KGgX*u$t438 zIx*YN$h8$FKke9wsg`rV=TQG=$T1@w=H$b7#LTX9G(QwVojE#l%bgN5H;;n2g!Td0 zFq`|lE3S-G&GS2SRh}vG^ZZLMQzk|J6&?9<8au3R`XT!GimmFQ>MOP?MZS^x_?he@ zBHx(*c8%EHwbA$G0#@!&j2r#VI+nDat)Sfe(c?a1L0i%M(|h8-i^COui*~AATFF-U z9c1>G^GdeT_7N)?EZs+1$X5Ct<79hS$zXOW8}8v|@T8Tbj z33L!2SW#PxTSh&fSur6*f%69|s)z6~f+mVSn^uNgw62CKp&5Cw#u@YOTpcT`Xzci$ej-c;T-+ee-J?bYDd7XXZduUYb%^Q%ihNLLSe z3%ClW?7Cd_Jr`G0R)PYZmkUnScUKL@Ssk|vT%qpG4bIh84dh8ufkF>d*F1bbNN4Y_ zcvzl)>v9j`yo<|P{~F1!uHxakY?q1x9yNo3cuI( zsHwS+LURvJ?(({be^3E=ZINBR8w(#UT!c?vNRc2E`HISii9A*lqVaIHX5hdfwenI~ za}VGlYPrz*#W3q@tM0uQZBvHf(xUDx#7SOSx@ODoe*pJbm6tO2=*mdD5oAlZWGvmVg zpX?SE%5Kt26im3?q?ag|nD3kXR(}?0;k#sx@)K=HYW*iwpU$P5mGclq;11?B`YD@g z<;FvJ->g?|nDD;YZ~GVJJ)*pC_DiqkVO^i`KK6?~Q=_^bzCQM&amBZ3NoaV;rh*0& z-gjzfFyVZshDN3g$xaQeSLVOG-G0#lHLPB6w%d=&gJLhI2ph&8-znoQ4klaXAOr#=*q{`m{d8zQNiH%z$rf_PI7_j2fzw4Yo& zB3)pw-k)E&QH%`t`+?j_Q3x*f`<><46DC~j_b<4RLv};PJLngEuf}!*N*?s1vf>RK zvO^jwOx%TZ$Y7!DAssT9@Onsx>;@UKLpo%5tf(H+A-jP?cG!A{J6JMgc*v#-8BBOT ztV0G9Av>%?My3qeVG0?6xxs16S2gnf9Fe&Jw`A{#?|hg4_suvq9r2B4qH%;gC%K*z zrE+b*^Iee3rn8PV4?5{-9wmgdqR)>M#lCtRsBC07tDPD0)=LCb&NKv96qx5j00OK+I4i^eAOaLA(+Kvo8wmAY1{QiZTB!FjFyV2u zP}hLo4EHHp6=h+`Lm3{j>C0ObOjLbJw3Yiu*qbSjv31dVSh_I9X^c<@f0RYtF+yFr zdh@kutgVWNu;e8d4^efj?J=E`$J!p#wW&ckp}aQrrUe0}Jbc2!X3`+kwFxF%HHeH% zG?;MJAj;&8vNs=}#|fuYaYI#DaQJr2OgaB7Y_bCy&_E)P;@G69g`5NzsB#4%tNEwn2GM3b-{1x4kXk zWI`yo{OT$cs!76aWvWQKoh^XyOrqfF8Wf!hQbz!afw0=*p{RiCj{aT9=r zyLm|H_?^ZxwpqFgJR>r#cuM!d?NSsyr>1ltv@%Voy}u7nnd#as%!~_5o@ij9>~!rF zCcIA9gMo?ePZyy&(vU3|jY5MT2de(Y}J6Lk1$3r&Nasd82&eiN-q91dK-5gr+`(?cOqG+**l;tDYUV|U3xi8nBFBN^C-XX?^ z^L3N`nB)RIR+t$VmLv%aZ7k3vVWN!%nj}n&b%6#+rX1@64YD7DY_!hd4c0Xxq?E-| zwiQX3@ZP9N!i4umO_EIMy-_E0Ki)^0tas!ytUEYE@DR?M^yUf^&YQIJ5H`FwiMF@% zsf0{;2j3=9dYf`U9|9Iy@2C%}r+x_U3x)cI(2rlqEEHZ|$z* z`e>i_n?=?;CJn4|<(+n)MYiv>`z+FZM^$)VB--{+-d*K=ktpph{)X81&v;8j(Hn?; zAKaVi(`H}(YJ_iZ{c;0yRS(qX$Ltb+xf34;EB{x4<=uNZk4NUZ= zS@)(thj=Mf{WBI7m>G9z{@l;S)fX=nzI+u55mlCoQn^z1=eRASD!sT0;g3i z0LHjnHw6kTY)ZkkUkJU#}AxCcLlL>kXOG`+B|J+{Ssk!FmS)SaQzdA)If}Tw%ib2JJkA z4euNDq$LyHfolUzTFR^2Gu}o~^pVP|+u>}Zh{{hew==U%x?z}j4Wg$fER@}(8-|I7 zH|d6NXCj+>34D~KO&>Pid+<33;fBSg`U(jXbvFz3jpBA5?-pwIIGa@F2F5H_A(x84uPrm6xzldaE7=O!(cZ z^AaZVa;s>&jq~#EDAgx|B$jSdiK#Sn^AnwzN&@IlDKF7S5AANpv`O=Y&7)Sgi)cIX zHeQ5#I6-H;-NG3gH1GaW{3R0Z79PK_rzh~cTl|(6a5B-q-Qt!ZG;hdsGu|HI9v2?n zhFMrTrkJ$gIEILMqct?RS9s?XO7Of-co(9k(4~qI<1fUYQLaG51;G7Mc)t;?Y{3%r z_$%QZ6fFupVo^CH@<;DUw~Ty~Ke}JE48Il8^9oBK9ghmHTcLy_@-g9ES}5Ti_;+WO z!q>X*g?D*jsaVGSfl65vCxkmDuqa+16N1GF5sV2+%z$1WQ%vdiqbU7tp+*_{lW5b$ zG=Oqo{j=~|7Z#&6`lLve85Sr;EPfH*(7>{Q3KqkHuz(u01WFDMyvKiC5&)hEyt@ip z27o66uYI8e0HXu1L!kr!PX%ZJnT-HOGG0UA%ntaPMmD+{HU!>zw7ZjycsB&+_d{_u zi!&t+!Ck{5iXUEfdE)|imf;t1IhDo*!7SjDsl*!mjt@*Ku>{u>0+UKCQ7TOg1MhbV zAB&VqlLD`^!p9ondphu*F;G|qzNu7D<{?Wh(8{zRabaO!kWbSClNMYIIG+u?KX9pD z7&Z+9JZA(Z?N|cO=gz7Lo-?V`;t~dmH&XKh=kuVrY<5XDf6H3Gb@PscVzLv?r_e~K zbeowUbhuKRNSXOT_rEKqxa@li0(T*pw&bbkxQ!pY(S^idp~*=p;(*W?c>kdWEH&I& zn*uLc;B8UhuIX(;$3W7BzM64l1i*1NlLPuYjKkmPyEyEQ-w43QG2vcxY22njhxj2IJwx!Mzg0m_xlbt2RW_93Q ztfI;iV)IVmbuE+-n|H~Voa{~rvhM}%t^yR8U!(=Q3cy06Y4CnvCcEQ8qsjh3U?w|j zh}_!1Om>!NvabuwWM_@QtPi|!Qvn%x1ewi+2qDpA-x7GA{kk9k{44NoGebi`0N8G4 zHn#!*J1G0*tYZlPc2WzE7gKU3umO5s2F}mO&L8o+oA5Qiee;z^#l4a6ODZL^vlv17 zGU(Dnn;7GlLEma^BIUjcT-}#qnVXP;TUBJG1Sekw*#QcDG05)^+@B2jVwr^dgWxAh z!jKc4Mc_MNvydhD`r2k8OO%BN?K;64trLgrI>8!|KOA^R4Nq19{+L}SAcZm)zq1Pk zmqRXoZ>Kv;2*nS2p(t`#LMV>gjAaR-IAL>kSb z(?PaUMFMXYy)%K^5Ys!JD9eM9)+_6I48y2$-pTW>k_qCPl^ST!BdV)iip?KG4IA-X;`fDF|$~cw<$4qUTC6I zBBL`U7R;mQ@HAl!!Fk38hb44$YRtPpt;j49^l35gIRl+lK%Ys_TZ({2L`}4aAY`*+ zSc<8~`8#p`37?OdV;Gk~4CYvrEFlIj#>~ph5@y3oHU=yq26NA+bNG^h?D(G>e1eN-eouv-{& zR~vMmM7J;&tTyOa1G+^P9ZT@M*rH>J(7hT9y=8_S%V77KWycb;dm|R@sv;{zxO~$B zWDNjUSPm=!;7SXSB>;RYrnk0B9M(Ya?Xzlv;3^{cwItXgsEi{(kd7XvE5-Wa6^2&om6x8;`+;0rEsHCM3D^E2L2H|7kF=lh-(H-$%I9{-LL3FD(5M`IcON=PP19F1Lats>!r z#Icw=EUtGGUy?W$3x>t@dc_(@d}m3p1kc~oW+G3JEK&aafc*K%({skqrhSt2}R;${zJ4S2@J%^u1U@HE8D9?BB%jEkG}VF`G~7gQ`$cqR}Y z`eiWq1Q~B~+<7J6E)u*FPL6y0TXE9SkIC`0elZb%&*b>USF7|1z-LO_ooDz2lFyWQ zFwgK|4Sb%ld{~0#sd2O2u|#~PfzNY>56j>)llaK}jwQL@#iN&~HCu{s`Fz}LdaME3 z96NTF0PV%N+4NWfw3p&`&1MbM=32HaA*Ws@Xa^*;SjKCNJ8$KWIl=z0G4AoN`~fY7 zel*5g={tA~&>G|2dMao!Kx>M-D-E=mgw_-fRvKum0op_*KAU;20pJ_J}iOH8+PCoLfPB?&O?`42+x}M`QBQChh^YdXLE`r&Z!Ogqa{Jh@I#wM{0x{j+0n8DOq*>Qu>?$8EGCwK z=_3o1C1laZL}{kn#PDKqd)zq^FD`3vve)MBtr~oNRsQ-K5pQnr0xuG7k9+(v1E}al z`1W|&JRZjzGvcv#~C8B>) z0Hp|*!xLsXV+{aDB+MSb5&(`&nB|Nm0DLr|m$S=4fl>k$CmzZ## z2f)vr{LE+kcFi{jLBB}Ykns519TG5wE;S@N^Cw#}!LK3Ft4i@pf#0}08WyJ?mm zYvA{+rN9#S&9MAf0>9@HdJnxsdIP_iXVnD1S;X&ExgDf4-irz6_56qNU~%|j!qaE= zH2U#k;y2f-q)P+Zi;3Pf3R)V_UP`#H8E9z zXbTK9mI1BNLSu=cElfoJO(mTa;c}6M#u}i#YKP7epe?b`SOT=>0%)uO+R}uXwJZVJ zGJ@7Dp=C1O@`STF-0jd9;eR`YUK7U!u;J?EC3G=eF6*{y(VP2ND0;v6ozO^dNS^?C7 zgnQC}Y9&D(NCYPhDAoY#YYU1ccs^)Bu|%K_0n`x#ie-R0YSWA*PP6Y4(aRJ)DZ=IV zHqBTAz2g=FOQ3hcrWs41_oJcr2ZuG_`{}Hjp!YM;qyM`Hldd&hH71;fWd4Iz!SwJ< z!fd0h(UCI=vyHX}wKEB`jkX50p-FdaQg5TJCAFc+U~E!vqpX42u%y`rSc2!_NwW>G zMARNhhTf>8-Ue6(wa1bsy;!33dOT^;i!}gxB58I^mH_ClLVl@F`qUWZ+z03lmEYMg1?62ljfM#2HhB+G{>|yz%xE+j%jUxXF}3_!Qg2l z@k~etFBm+m0nbE>hb4HPWbv>>c%Dw4?>%Giunas?36H!qvBXI=JwL214!F-I&B217 z0n>AKv@8MB%%nM3umnuAEGCwKY4%wakw(uGrT=6~ZLz;4oi~!jWqq@^4yOMnr$C-OUDCsf4yN8EiJt zSOc`TEHswj`E3i0C4#mJ(AF4eECbqm78*+o?SrJ*B&7(KYb`X^0ByY;I!l1I!9rsR z&^|QKE_YZ1w2f!g1hh>AjsC9-tX}Q#gN>x~HS(?wA9CmTZSs}vgL}iRNwbW#Lyxv5 z%`(;w__ij^GS&|GK1sR<3_e}~NDB@ae5?WArxqVe@Vw39V~Oy627EgWK9+%Rm&L~t z1HHqRUK!|plXQos^d`V>ok$CYrt~Jj8t8p% z>9GXQM=U*-h~81q`@zs-8T5`@dMq)$A8qnU5iWnS^jHJEla>KXp!bWV#}eqBGW4!+ zSOdM&XVnC~Geqw_nSAZ>;xOer4Sda!Y`$@>-#Wjj6JD*2Oqmt0J-Re9WmdfQ;5Ra5 zR=oD$_h`zUWcan0llRe7Fv;*^4g5x>%qGebJU^B)nbgo27$8U#>HOaavqiK;`!o0@VK z0M#y_YTAiZn~)8*gi}*yqIN)+rl!n9?Eq9$Q)Z%e0IF#zcfLW@L86+L3g#PBtO3<@ zi;5+9e%7L5iBQb|s@Vn=%Ru!!p^_UaOPp$RQs&wzMYw!1Wp-EAz;CX_z!LbqoHDyB zOW-%p@Vmlc4FF#`t0wsUllXlX<(b+Mt8&Wu6#Vu?v-wC9-g_+Q65Qs7t1Q5d=+i0- zupMA0$iO6J~04U1HgAIK$hV7T?>#U0=NbM*BXE<1K>IfkR=AV zfdERXN<=dsTB=e+I~y%k)WCwUeZ} zHx(Q+R9VAt_gShe!SffEDoaH5OHlpVP-PiZ584^b63yU4b_TNsR)_6~SputX>Sy1FNIN>QD=2)fwmWwA0i)>cZe)arj8ueDUp!jy#e!Uwk_Q&?9N{ z#kVs6jYzwb(|U{G8ysoDg zGr?H`-brb*K(Yk9Pp8c?$`a`2$(j6ENtC9f-Nph+`1ufNK_e*Xp+ch{Jw1~S&99z1 z>t8)hO`Bgmv4${AqaYR3rC&WwPn%yoaWTU5Y}%VufC;St)9gZ?kmyHG&!@e4T<|XX z^(of}fLGGq&FVuYO91fCwE2l6O8_uGjg?;hHi9JpSU^q8lp91A-#60EnlxUJNAfE! z4%*~*UmA=I7n39TS~rW3EKZxPG>ZW)PIuSu#j_yzYTA9*5aeBmwBTKnXRHCj5}Ri% z!E>|CGnOdNmZtw?elo)%<=HYyADL&Y5y02d-f{zwRRCO(|4p}`<=qy(6T79??EQa&lDirJ delta 18898 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 diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/datasource.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/datasource.proto index 058fbcac2..d64bd7cc9 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/datasource.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/datasource.proto @@ -1034,15 +1034,23 @@ message RevokeSourceDelegationResponse { // the capability that admitted it and revoking the delegation refuses the // next occurrence. // -// IT IS SET ONLY FOR A DELEGATION THE EXECUTING MODULE CAN USE. A mint by -// source id resolves the active delegation of the CALLING module, and the -// capability is addressed to the binding's declared audience, which the -// executing module must itself admit — so the one shape a declared -// reference can serve is a delegation whose binding names its own module -// as the audience. A delegation held by one module and addressed to -// another is reachable only through the delegation-id exchange, which no -// reference this host declares can name, and declaring it would read as -// bound while every occurrence was refused at the mint. +// IT IS SET ONLY FOR A DELEGATION SOME DECLARED EXECUTOR COULD USE, which +// is weaker than naming the one that will. A mint by source id resolves +// the active delegation of the CALLING module, and the capability is +// addressed to the binding's declared audience, which the executing module +// must itself admit — so the one shape a declared reference can serve is a +// delegation whose binding names its own module as the audience. A +// delegation held by one module and addressed to another is reachable only +// through the delegation-id exchange, which no reference this host +// declares can name. +// +// WHICH module that is, this host does not know: nothing it holds declares +// who executes declared work. `identity_holder` on the declaration read +// reports whose delegation the reference resolves to, and executor +// readiness is deliberately not claimed — a composed executor with no +// delegation on this source is refused at the mint +// (ErrSourceDelegationMissing), and a host that called the source "bound" +// would have been telling an operator the opposite. // // IT IS NOT OPAQUE, and revocation is therefore not permanent. The value // is the source's own id, and a mint resolves whichever delegation of that @@ -1207,6 +1215,19 @@ message SourceTriggerDeclaration { // held off its schedule: then there is no pass this host will take. bool host_sweep_enabled = 9; google.protobuf.Timestamp host_sweep_due_at = 10; + // identity_holder is the module whose delegation the reference arm resolved + // to when this declaration was built. The arm cannot carry it — the + // reference value is the source id — so it is reported beside it. + // + // IT IS NOT THE EXECUTING MODULE. Nothing this host holds declares which + // module executes declared work: a self-addressed delegation binding is + // generic and a deployment may have several. So this says "a reference on + // this source would resolve to module A's self-addressed delegation", and + // whether A is the module that installs this release is unverified here. A + // reader that took it for executor readiness would be told a source was + // ready while every occurrence of another executor's trigger was refused at + // the mint. Empty when the arm names no identity. + string identity_holder = 11; } message DelegateSourceTriggersRequest { diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index c7bc3db16ebdf9478699d4def77e8a66997bff99..70814cf1f0777d58cd32b767ba3d27fd4653ca82 100644 GIT binary patch delta 20350 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 delta 18898 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 diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/datasource.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/datasource.proto index 058fbcac2..d64bd7cc9 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/datasource.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/datasource.proto @@ -1034,15 +1034,23 @@ message RevokeSourceDelegationResponse { // the capability that admitted it and revoking the delegation refuses the // next occurrence. // -// IT IS SET ONLY FOR A DELEGATION THE EXECUTING MODULE CAN USE. A mint by -// source id resolves the active delegation of the CALLING module, and the -// capability is addressed to the binding's declared audience, which the -// executing module must itself admit — so the one shape a declared -// reference can serve is a delegation whose binding names its own module -// as the audience. A delegation held by one module and addressed to -// another is reachable only through the delegation-id exchange, which no -// reference this host declares can name, and declaring it would read as -// bound while every occurrence was refused at the mint. +// IT IS SET ONLY FOR A DELEGATION SOME DECLARED EXECUTOR COULD USE, which +// is weaker than naming the one that will. A mint by source id resolves +// the active delegation of the CALLING module, and the capability is +// addressed to the binding's declared audience, which the executing module +// must itself admit — so the one shape a declared reference can serve is a +// delegation whose binding names its own module as the audience. A +// delegation held by one module and addressed to another is reachable only +// through the delegation-id exchange, which no reference this host +// declares can name. +// +// WHICH module that is, this host does not know: nothing it holds declares +// who executes declared work. `identity_holder` on the declaration read +// reports whose delegation the reference resolves to, and executor +// readiness is deliberately not claimed — a composed executor with no +// delegation on this source is refused at the mint +// (ErrSourceDelegationMissing), and a host that called the source "bound" +// would have been telling an operator the opposite. // // IT IS NOT OPAQUE, and revocation is therefore not permanent. The value // is the source's own id, and a mint resolves whichever delegation of that @@ -1207,6 +1215,19 @@ message SourceTriggerDeclaration { // held off its schedule: then there is no pass this host will take. bool host_sweep_enabled = 9; google.protobuf.Timestamp host_sweep_due_at = 10; + // identity_holder is the module whose delegation the reference arm resolved + // to when this declaration was built. The arm cannot carry it — the + // reference value is the source id — so it is reported beside it. + // + // IT IS NOT THE EXECUTING MODULE. Nothing this host holds declares which + // module executes declared work: a self-addressed delegation binding is + // generic and a deployment may have several. So this says "a reference on + // this source would resolve to module A's self-addressed delegation", and + // whether A is the module that installs this release is unverified here. A + // reader that took it for executor readiness would be told a source was + // ready while every occurrence of another executor's trigger was refused at + // the mint. Empty when the arm names no identity. + string identity_holder = 11; } message DelegateSourceTriggersRequest { diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index f6e272fda..03b42b5d1 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:49b6defafbf8aa3ad53bb675fd165c1896a51113da9d30edc0364fc34a7673bf", + "digest": "sha256:c57b6743854c84738f0f1454e6c93349eced569d0351a91a62786ba08c1bc806", "services": [ { "name": "ModuleCapabilitiesService", @@ -65,7 +65,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:49b6defafbf8aa3ad53bb675fd165c1896a51113da9d30edc0364fc34a7673bf", + "digest": "sha256:c57b6743854c84738f0f1454e6c93349eced569d0351a91a62786ba08c1bc806", "services": [ { "name": "APIKeyService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 227c8e940..fefbd9977 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:49b6defafbf8aa3ad53bb675fd165c1896a51113da9d30edc0364fc34a7673bf + digest: sha256:c57b6743854c84738f0f1454e6c93349eced569d0351a91a62786ba08c1bc806 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:49b6defafbf8aa3ad53bb675fd165c1896a51113da9d30edc0364fc34a7673bf + digest: sha256:c57b6743854c84738f0f1454e6c93349eced569d0351a91a62786ba08c1bc806 - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index 0a5db40ea..6de048ee2 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -921,14 +921,27 @@ the values and records the other column beside them). revocation land on the NEXT occurrence rather than behind something held from the one before. - `DeclareSourceTrigger` sets it **only for a delegation the executing module - can use** (#1121): a mint by source id resolves the delegation of the CALLING - module and the capability is addressed to the binding's audience, which the - executing module must itself admit — so the one usable shape is a delegation - whose binding names its own module as the audience - (`TriggerDelegationReference.usableByExecutor`). It took the first active - reference from any module before, so a deployment whose executor held none - read as "bound" while every occurrence was refused at the mint. + `DeclareSourceTrigger` sets it only for a **self-addressed** delegation + (`TriggerDelegationReference.candidateForADeclaredExecutor`, #1121): a mint by + source id resolves the delegation of the CALLING module and the capability is + addressed to the binding's audience, which the executing module must itself + admit, so that is the one shape a source-id reference can serve. It took the + first active reference from any module before, which named delegations no + trigger could ever use. + + **That is a shape, not an executor, and the read says so** (#1121, F1). + Nothing this host holds declares which module executes declared work: + self-addressed bindings are generic and a deployment may have several. So a + source whose only self-addressed delegation belongs to module A is a + *candidate* under A's name — `SourceTriggerDeclaration.IdentityHolder`, which + the arm itself cannot carry because its value is the source id — and if the + composed executor is B, holding none, B's mint resolves + `ActiveSourceDelegation(S, B)` and is refused with + `ErrSourceDelegationMissing`. Claiming "the executing module holds it" would + have told an operator a source was ready while every occurrence failed. + Resolving it properly needs the executing principal named by a contract that + does not exist yet; a sibling module's prefix is not that contract and is not + hard-coded here. - **`binding` is the installation-principal arm, and PLANNED.** It would name one of the release's bindings and mint from that binding's `headless_scopes` under the module's own principal (`MintModuleOperationContext`) — an ordinary @@ -1035,9 +1048,12 @@ the values and records the other column beside them). alone, and a stored instant aligned to the cron would make every source of one provider come due in the same second. `DATASOURCE_RECONCILE_SWEEP_ENABLED` (application group, ON unless `false`) is what an operator turns off when a - composed module starts consuming the declaration; it is read in two places - that are one fact — the tick is not installed, and `RunDatasourceReconcile` - itself refuses. + composed module starts consuming the declaration. The refusal is in + `RunDatasourceReconcile` itself: the one-minute tick still fires and its + callback still runs, and the sweep returns having read nothing and enqueued + nothing. That is deliberate — a switch read only where the ticker is + installed would leave every other caller of the sweep unguarded — but it is a + sweep that does nothing, not a tick that was never scheduled. ## A module identity minted for one tenant diff --git a/module/services/accounts/code/pkg/adapters/datasource_trigger.go b/module/services/accounts/code/pkg/adapters/datasource_trigger.go index db9426d57..12673bd18 100644 --- a/module/services/accounts/code/pkg/adapters/datasource_trigger.go +++ b/module/services/accounts/code/pkg/adapters/datasource_trigger.go @@ -235,6 +235,8 @@ func triggerDeclarationToProto(declaration business.SourceTriggerDeclaration) *g Operation: declaration.Operation, Digest: declaration.Digest, UndeclarableReason: declaration.UndeclarableReason, + IdentityHolder: declaration.IdentityHolder, + HostSweepEnabled: declaration.HostSweepEnabled, } if declaration.LastOccurrenceAt != nil { out.LastOccurrenceAt = timestamppb.New(*declaration.LastOccurrenceAt) @@ -242,6 +244,13 @@ func triggerDeclarationToProto(declaration business.SourceTriggerDeclaration) *g if declaration.NextOccurrenceAt != nil { out.NextOccurrenceAt = timestamppb.New(*declaration.NextOccurrenceAt) } + // Both halves of "what this host will do about the schedule", and they are + // set OUTSIDE the undeclarable return below: a source that declares no + // trigger is still swept while the stopgap runs, so a reader of a parked or + // uncron-able source must still be told whether this host is firing. + if declaration.HostSweepDueAt != nil { + out.HostSweepDueAt = timestamppb.New(*declaration.HostSweepDueAt) + } if !declaration.Declarable() { return out } diff --git a/module/services/accounts/code/pkg/adapters/datasource_trigger_identity_test.go b/module/services/accounts/code/pkg/adapters/datasource_trigger_identity_test.go index aa8a4b875..90e68f3d5 100644 --- a/module/services/accounts/code/pkg/adapters/datasource_trigger_identity_test.go +++ b/module/services/accounts/code/pkg/adapters/datasource_trigger_identity_test.go @@ -87,6 +87,39 @@ func TestTriggerOccurrenceMintsFromTheReferenceAndIsRefusedAfterRevocation(t *te require.Len(t, audit.of(business.EventSourceDelegationUsed), 2, "a refused occurrence mints nothing and records no child") } +// A DELEGATION ONE MODULE HOLDS IS NOT A CAPABILITY ANOTHER CAN MINT +// (issue #1121, F1), at the real mint route. +// +// This is the second half of the finding the declaration half corrects. The +// counterexample: source S has a live delegation held by module A, and the +// composed executor is module B. The declaration can only name S — the +// reference value is the source id — so a host that called S "bound" would be +// claiming readiness for B. It is not: a mint by source id resolves +// ActiveSourceDelegation(S, caller) and B has none. +// +// Asserted here rather than reasoned about, because the whole correction rests +// on the mint refusing B while admitting A. +func TestAnotherModulesDelegationDoesNotMintForThisOne(t *testing.T) { + store, audit := installSourceDelegationService(t) + + // A holds the delegation and mints from the source id. + held, err := mintSourceContext("docstore", moduleToken(t, "docstore"), business.SourceDelegationRef{SourceID: delegationSource}) + require.NoError(t, err) + require.NotEmpty(t, held.GetToken()) + require.Len(t, audit.of(business.EventSourceDelegationUsed), 1) + + // B presents the same reference and is refused — as a decision, not an + // outage, so a module reading it does not retry it as transient. By source, + // "another module holds it" is indistinguishable from "nobody does", which + // is deliberate: B learns nothing about A's delegation. + _, err = mintSourceContext("reports", moduleToken(t, "reports"), business.SourceDelegationRef{SourceID: delegationSource}) + requireRefusal(t, err, codes.FailedPrecondition, SourceDelegationMissingReason) + require.Len(t, audit.of(business.EventSourceDelegationUsed), 1, + "a refused mint issues nothing and records no child") + // The delegation A holds is untouched by B's attempt. + require.NotNil(t, store.delegations[delegationID]) +} + // The headless mint a binding with its own audience and headless scopes // answers: a capability for work with no person anywhere in it. // diff --git a/module/services/accounts/code/pkg/adapters/datasource_trigger_projection_test.go b/module/services/accounts/code/pkg/adapters/datasource_trigger_projection_test.go new file mode 100644 index 000000000..8aebbcc03 --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/datasource_trigger_projection_test.go @@ -0,0 +1,103 @@ +package adapters + +import ( + "testing" + "time" + + "accounts/pkg/business" + + "github.com/stretchr/testify/require" +) + +// EVERY FIELD OF THE DECLARATION REACHES THE WIRE (issue #1121). +// +// This test exists because a field that was never projected shipped: the +// declaration gained `HostSweepEnabled` and `HostSweepDueAt`, the panel was +// written to read them, and `triggerDeclarationToProto` was not updated — so +// the page would have told every operator that this host fires nothing, which +// is the opposite of the truth and exactly the claim the change was made to +// correct. Nothing caught it, because the panel's container test serves its own +// JSON fixture and never crosses this projection. +// +// So the assertion is deliberately field-by-field rather than "the important +// ones": a projection is a place where a value is silently dropped, and the +// only test that notices is one that names everything. +func TestTriggerDeclarationProjectionCarriesEveryField(t *testing.T) { + last := time.Date(2026, 10, 12, 9, 0, 0, 0, time.UTC) + next := time.Date(2026, 10, 12, 9, 30, 0, 0, time.UTC) + due := time.Date(2026, 10, 12, 9, 7, 0, 0, time.UTC) + declaration := business.SourceTriggerDeclaration{ + Release: business.DatasourceReconcileRelease, + Operation: business.DatasourceReconcileOperation, + Schedule: business.DeclaredTriggerSchedule{ + Cron: "*/30 * * * *", Timezone: "UTC", MissedRun: "skip", Overlap: "skip", + Deadline: 10 * time.Minute, Input: []byte(`{"org_id":"o","source_id":"s"}`), + }, + Identity: business.DeclaredTriggerIdentity{Source: "a-source"}, + IdentityHolder: "module-a", + Digest: "4f1c", + LastOccurrenceAt: &last, + NextOccurrenceAt: &next, + HostSweepEnabled: true, + HostSweepDueAt: &due, + } + + out := triggerDeclarationToProto(declaration) + + require.Equal(t, business.DatasourceReconcileRelease, out.GetRelease()) + require.Equal(t, business.DatasourceReconcileOperation, out.GetOperation()) + require.Equal(t, "4f1c", out.GetDigest()) + require.Equal(t, "*/30 * * * *", out.GetSchedule().GetCron()) + require.Equal(t, "UTC", out.GetSchedule().GetTimezone()) + require.Equal(t, "skip", out.GetSchedule().GetMissedRun()) + require.Equal(t, "skip", out.GetSchedule().GetOverlap()) + require.Equal(t, int64(10*time.Minute), out.GetSchedule().GetDeadline()) + require.JSONEq(t, `{"org_id":"o","source_id":"s"}`, out.GetSchedule().GetInputJson()) + require.Equal(t, "a-source", out.GetIdentity().GetSource()) + require.Empty(t, out.GetIdentity().GetBinding()) + // The holder is NOT the executor, and the field's own comment says so; what + // matters here is that a reader is given it at all (issue #1121, F1). + require.Equal(t, "module-a", out.GetIdentityHolder()) + require.Equal(t, last, out.GetLastOccurrenceAt().AsTime()) + require.Equal(t, next, out.GetNextOccurrenceAt().AsTime()) + // What THIS HOST will do about the schedule, which is a different fact from + // the cron above and the reason both are carried. + require.True(t, out.GetHostSweepEnabled()) + require.Equal(t, due, out.GetHostSweepDueAt().AsTime()) + require.NotEqual(t, out.GetNextOccurrenceAt().AsTime(), out.GetHostSweepDueAt().AsTime(), + "fixture: the declared instant and the swept instant must differ, which is the finding they report") +} + +// A source that declares no trigger still says what this host is doing, because +// the stopgap sweeps a source whose cron could not be spelled just the same. +// The schedule and identity are withheld; the sweep's state is not. +func TestAnUndeclarableSourceStillReportsTheHostSweep(t *testing.T) { + due := time.Date(2026, 10, 12, 9, 7, 0, 0, time.UTC) + out := triggerDeclarationToProto(business.SourceTriggerDeclaration{ + Release: business.DatasourceReconcileRelease, + Operation: business.DatasourceReconcileOperation, + UndeclarableReason: business.TriggerUndeclarableInterval, + HostSweepEnabled: true, + HostSweepDueAt: &due, + }) + + require.Equal(t, business.TriggerUndeclarableInterval, out.GetUndeclarableReason()) + require.Nil(t, out.GetSchedule(), "an undeclarable source has no schedule to project") + require.Nil(t, out.GetIdentity()) + require.True(t, out.GetHostSweepEnabled()) + require.Equal(t, due, out.GetHostSweepDueAt().AsTime()) +} + +// And with the switch off there is no instant to report: an operator must not +// be shown a pass this host will not take. +func TestASweepThatIsOffNamesNoInstant(t *testing.T) { + out := triggerDeclarationToProto(business.SourceTriggerDeclaration{ + Release: business.DatasourceReconcileRelease, + Operation: business.DatasourceReconcileOperation, + Schedule: business.DeclaredTriggerSchedule{Cron: "*/30 * * * *"}, + HostSweepEnabled: false, + }) + + require.False(t, out.GetHostSweepEnabled()) + require.Nil(t, out.GetHostSweepDueAt()) +} diff --git a/module/services/accounts/code/pkg/business/datasource_deliveries.go b/module/services/accounts/code/pkg/business/datasource_deliveries.go index be5713230..5320676f9 100644 --- a/module/services/accounts/code/pkg/business/datasource_deliveries.go +++ b/module/services/accounts/code/pkg/business/datasource_deliveries.go @@ -907,10 +907,12 @@ func (s *Service) allocateOrdinal(ctx context.Context, sourceID string) (int64, // switch whose default silently stopped every scheduled reconcile would be the // outage #1090 already caused once. // -// It is read in two places and they are the same fact: the tick that fires the -// sweep is not installed, and the sweep itself refuses — a host that only -// dropped the tick would still sweep from any other caller, and the -// declaration's own read would still describe a sweep that is not running. +// The sweep itself refuses, which is where the guard belongs: a switch read +// only where the ticker is installed would leave every other caller of +// RunDatasourceReconcile unguarded, and the declaration's own read would still +// describe a sweep that is not running. The one-minute tick in work.go goes on +// firing and its callback goes on being called; what it finds is a sweep that +// reads nothing and enqueues nothing. // // STOPGAP (issue #1112): deleted with RunDatasourceReconcile, and listed in the // block above. diff --git a/module/services/accounts/code/pkg/business/datasource_trigger.go b/module/services/accounts/code/pkg/business/datasource_trigger.go index d35b92e44..7bd8f5413 100644 --- a/module/services/accounts/code/pkg/business/datasource_trigger.go +++ b/module/services/accounts/code/pkg/business/datasource_trigger.go @@ -196,8 +196,18 @@ type SourceTriggerDeclaration struct { // on its schedule: then there is no pass this host will take. // // STOPGAP (issue #1112): both go with RunDatasourceReconcile. - HostSweepEnabled bool - HostSweepDueAt *time.Time + HostSweepEnabled bool + HostSweepDueAt *time.Time + // IdentityHolder is the module whose delegation the reference arm resolved + // to when this declaration was built, and it is reported because the arm + // itself cannot carry it: the reference value is the source id. + // + // It is NOT the executing module (issue #1121, F1). Nothing declares who + // executes declared work, so this says "module A's self-addressed + // delegation is what a reference on this source would resolve to", and + // whether A is the module that installs this release is unverified here. + // Empty when the arm names no identity. + IdentityHolder string UndeclarableReason string } @@ -229,12 +239,29 @@ type TriggerDelegationReference struct { RevokedReason string } -// usableByExecutor reports whether a declared trigger's reference arm can name -// this delegation: it is live, it has a reference value, its binding still -// confers something, and the module that holds it is the audience of its own -// binding — the only shape the module that fires the trigger can both mint from -// a source id and admit as its own execution context (DeclareSourceTrigger). -func (r TriggerDelegationReference) usableByExecutor() bool { +// candidateForADeclaredExecutor reports whether a declared trigger's reference +// arm MAY name this delegation: it is live, it has a reference value, its +// binding still confers something, and the module that holds it is the audience +// of its own binding — the only shape a source-id reference can serve, because +// a mint by source id resolves the delegation of the CALLING module and the +// capability is addressed to the binding's audience. +// +// WHAT IT DOES NOT ESTABLISH, and the name says so on purpose (issue #1121, +// F1): that the module holding it is the module which will execute this +// release. Nothing in the envelope or the derived registry declares who +// executes declared work — self-addressed bindings are generic, and a +// deployment may have several — so "module A holds a self-addressed delegation +// on source S" does not imply "the composed executor B can mint for S". B's +// mint resolves ActiveSourceDelegation(S, B) and is correctly refused with +// ErrSourceDelegationMissing; what would have been wrong is this host telling +// an operator that S was ready. +// +// So the declaration names the candidate and the read reports WHICH module +// holds it (SourceTriggerDeclaration.IdentityHolder), and neither claims +// executor readiness. Resolving it properly needs the executing principal to be +// named by a contract that does not exist yet; hard-coding a sibling module's +// prefix here is not that contract. +func (r TriggerDelegationReference) candidateForADeclaredExecutor() bool { return r.Active && r.Reference != "" && len(r.Scopes) > 0 && r.Audience != "" && r.Audience == r.Module } @@ -467,11 +494,15 @@ func DeclareSourceTrigger( Deadline: DatasourceTriggerDeadline, Input: datasourceTriggerInput(source.OrgID, source.ID), } - // The executing module's own delegation, and nothing else — see the m5 note - // above for why that is the self-admitting one. + // A delegation a declared executor COULD use — the self-addressed one, for + // the reasons in candidateForADeclaredExecutor, which also says why this is + // not proof that the composed executor holds it. The holder travels with it + // so a reader is told whose delegation the reference resolves to today + // rather than being left to infer an executor. for _, reference := range references { - if reference.usableByExecutor() { + if reference.candidateForADeclaredExecutor() { declaration.Identity = DeclaredTriggerIdentity{Source: reference.Reference} + declaration.IdentityHolder = reference.Module break } } diff --git a/module/services/accounts/code/pkg/business/datasource_trigger_test.go b/module/services/accounts/code/pkg/business/datasource_trigger_test.go index d81d82f38..20736f7b9 100644 --- a/module/services/accounts/code/pkg/business/datasource_trigger_test.go +++ b/module/services/accounts/code/pkg/business/datasource_trigger_test.go @@ -205,13 +205,19 @@ func TestTriggerDeclarationPrefersTheReferenceArmAndFingerprintsItself(t *testin require.Zero(t, edited.NextOccurrenceAt.Minute()%15) } -// executorReference is a delegation the module that FIRES the trigger can -// actually use: it holds the delegation itself, its binding still confers -// something, and that binding names its own module as the audience. +// executorReference is a delegation a declared executor COULD use: the module +// holds it itself, its binding still confers something, and that binding names +// its own module as the audience. func executorReference(sourceID string, active bool) business.TriggerDelegationReference { + return selfAddressedReference(sourceID, "runtime", active) +} + +// selfAddressedReference is the same shape under a named holder, for the cases +// that turn on WHICH module holds it. +func selfAddressedReference(sourceID, module string, active bool) business.TriggerDelegationReference { return business.TriggerDelegationReference{ - Reference: sourceID, DelegationID: "d1", Module: "runtime", Binding: "occurrences", - Audience: "runtime", Scopes: []string{"collections:write"}, Active: active, + Reference: sourceID, DelegationID: "d-" + module, Module: module, Binding: "occurrences", + Audience: module, Scopes: []string{"collections:write"}, Active: active, } } @@ -471,6 +477,67 @@ func TestStopgapSweepLeavesTheDeclarationUnchanged(t *testing.T) { require.Equal(t, before.Digest, after.Digest, "the sweep moved the declaration an installation pinned") } +// A SELF-ADDRESSED DELEGATION DOES NOT IDENTIFY THE EXECUTOR (issue #1121, F1). +// +// The predicate above establishes the SHAPE a source-id reference can serve. It +// cannot establish who will execute the release, because nothing this host +// holds declares that: a self-addressed binding is generic and a deployment may +// have several. So the counterexample the review built is real — module A holds +// a self-addressed delegation on source S, the composed executor is B, which +// holds none, and B's mint resolves ActiveSourceDelegation(S, B) and is refused. +// +// What this host must therefore NOT do is tell an operator that S is ready. It +// names the candidate, reports WHOSE delegation it is, and claims nothing about +// the executor. These cases vary the holder independently of any executor, which +// is the axis the first version of this test did not vary at all. +func TestTriggerDeclarationNamesTheHolderAndClaimsNoExecutor(t *testing.T) { + next := time.Now().Add(time.Hour) + source := &business.DatasourceSource{ + ID: "2a3f5c1e-0000-4000-8000-000000000005", OrgID: "org", + Status: "active", ReconcileInterval: 30 * time.Minute, NextReconcileAt: &next, + } + // Two modules, each self-addressed, which the registry accepts generically: + // A holds a delegation on this source, B does not. + holderA := selfAddressedReference(source.ID, "module-a", true) + + // A ONLY: the declaration names the candidate and says it is A's. It must + // not be read as "the executor holds one" — if the composed executor is B, + // every occurrence is refused at the mint, and this host cannot tell. + onlyA := business.DeclareSourceTrigger(source, []business.TriggerDelegationReference{holderA}, nil, true) + require.Equal(t, source.ID, onlyA.Identity.Source, "the candidate is still declared; it is the only shape a reference can take") + require.Equal(t, "module-a", onlyA.IdentityHolder, "a reader must be told whose delegation the reference resolves to") + + // B's own current delegation yields the positive claim under B's name — + // the same declared reference value, a different holder, which is exactly + // the fact the arm cannot carry and the read must. + holderB := selfAddressedReference(source.ID, "module-b", true) + onlyB := business.DeclareSourceTrigger(source, []business.TriggerDelegationReference{holderB}, nil, true) + require.Equal(t, source.ID, onlyB.Identity.Source) + require.Equal(t, "module-b", onlyB.IdentityHolder) + require.Equal(t, onlyA.Identity.Source, onlyB.Identity.Source, + "the reference value is the source id, so A's and B's are indistinguishable in the arm — which is why the holder is reported beside it") + + // Revoking B's delegation removes the claim entirely rather than falling + // back to A: a declaration is built from the references as they are. + revokedB := selfAddressedReference(source.ID, "module-b", false) + goneB := business.DeclareSourceTrigger(source, []business.TriggerDelegationReference{revokedB}, nil, true) + require.False(t, goneB.Identity.Declared(), "a revoked delegation is not a candidate") + require.Empty(t, goneB.IdentityHolder, "no identity means no holder to name") + + // And a binding B's module no longer declares: the read resolves no scopes, + // so the next mint would revoke it as binding_changed. + staleB := selfAddressedReference(source.ID, "module-b", true) + staleB.Scopes = nil + changedB := business.DeclareSourceTrigger(source, []business.TriggerDelegationReference{staleB}, nil, true) + require.False(t, changedB.Identity.Declared()) + require.Empty(t, changedB.IdentityHolder) + + // With both present the holder named is one of the two that actually hold + // one — never a module that holds none. + both := business.DeclareSourceTrigger(source, []business.TriggerDelegationReference{holderA, holderB}, nil, true) + require.Contains(t, []string{"module-a", "module-b"}, both.IdentityHolder) +} + // THE STOPGAP SWEEP HAS A SWITCH, AND IT IS ONE FACT (issue #1121, N7). // // The declaration's consumer and the sweep must never both run: they enqueue diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/datasource.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/datasource.pb.go index d7d1e6868..1b0f52190 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/datasource.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/datasource.pb.go @@ -5496,15 +5496,23 @@ func (x *RevokeSourceDelegationResponse) GetDelegation() *SourceDelegation { // the capability that admitted it and revoking the delegation refuses the // next occurrence. // -// IT IS SET ONLY FOR A DELEGATION THE EXECUTING MODULE CAN USE. A mint by -// source id resolves the active delegation of the CALLING module, and the -// capability is addressed to the binding's declared audience, which the -// executing module must itself admit — so the one shape a declared -// reference can serve is a delegation whose binding names its own module -// as the audience. A delegation held by one module and addressed to -// another is reachable only through the delegation-id exchange, which no -// reference this host declares can name, and declaring it would read as -// bound while every occurrence was refused at the mint. +// IT IS SET ONLY FOR A DELEGATION SOME DECLARED EXECUTOR COULD USE, which +// is weaker than naming the one that will. A mint by source id resolves +// the active delegation of the CALLING module, and the capability is +// addressed to the binding's declared audience, which the executing module +// must itself admit — so the one shape a declared reference can serve is a +// delegation whose binding names its own module as the audience. A +// delegation held by one module and addressed to another is reachable only +// through the delegation-id exchange, which no reference this host +// declares can name. +// +// WHICH module that is, this host does not know: nothing it holds declares +// who executes declared work. `identity_holder` on the declaration read +// reports whose delegation the reference resolves to, and executor +// readiness is deliberately not claimed — a composed executor with no +// delegation on this source is refused at the mint +// (ErrSourceDelegationMissing), and a host that called the source "bound" +// would have been telling an operator the opposite. // // IT IS NOT OPAQUE, and revocation is therefore not permanent. The value // is the source's own id, and a mint resolves whichever delegation of that @@ -5999,8 +6007,21 @@ type SourceTriggerDeclaration struct { // held off its schedule: then there is no pass this host will take. HostSweepEnabled bool `protobuf:"varint,9,opt,name=host_sweep_enabled,json=hostSweepEnabled,proto3" json:"host_sweep_enabled,omitempty"` HostSweepDueAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=host_sweep_due_at,json=hostSweepDueAt,proto3" json:"host_sweep_due_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // identity_holder is the module whose delegation the reference arm resolved + // to when this declaration was built. The arm cannot carry it — the + // reference value is the source id — so it is reported beside it. + // + // IT IS NOT THE EXECUTING MODULE. Nothing this host holds declares which + // module executes declared work: a self-addressed delegation binding is + // generic and a deployment may have several. So this says "a reference on + // this source would resolve to module A's self-addressed delegation", and + // whether A is the module that installs this release is unverified here. A + // reader that took it for executor readiness would be told a source was + // ready while every occurrence of another executor's trigger was refused at + // the mint. Empty when the arm names no identity. + IdentityHolder string `protobuf:"bytes,11,opt,name=identity_holder,json=identityHolder,proto3" json:"identity_holder,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SourceTriggerDeclaration) Reset() { @@ -6103,6 +6124,13 @@ func (x *SourceTriggerDeclaration) GetHostSweepDueAt() *timestamppb.Timestamp { return nil } +func (x *SourceTriggerDeclaration) GetIdentityHolder() string { + if x != nil { + return x.IdentityHolder + } + return "" +} + type DelegateSourceTriggersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -7630,7 +7658,7 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "\x06scopes\x18\x05 \x03(\tR\x06scopes\x12\x16\n" + "\x06lookup\x18\x06 \x01(\bR\x06lookup\x127\n" + "\tminted_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\bmintedAt\x12\x16\n" + - "\x06record\x18\b \x01(\tR\x06record\"\xaa\x04\n" + + "\x06record\x18\b \x01(\tR\x06record\"\xd3\x04\n" + "\x18SourceTriggerDeclaration\x12\x18\n" + "\arelease\x18\x01 \x01(\tR\arelease\x12\x1c\n" + "\toperation\x18\x02 \x01(\tR\toperation\x12E\n" + @@ -7642,7 +7670,8 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "\x13undeclarable_reason\x18\b \x01(\tR\x12undeclarableReason\x12,\n" + "\x12host_sweep_enabled\x18\t \x01(\bR\x10hostSweepEnabled\x12E\n" + "\x11host_sweep_due_at\x18\n" + - " \x01(\v2\x1a.google.protobuf.TimestampR\x0ehostSweepDueAt\"g\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\x0ehostSweepDueAt\x12'\n" + + "\x0fidentity_holder\x18\v \x01(\tR\x0eidentityHolder\"g\n" + "\x1dDelegateSourceTriggersRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12%\n" + "\tsource_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\bsourceId\"p\n" + diff --git a/module/services/accounts/code/work.go b/module/services/accounts/code/work.go index 20ac4a1a1..180e3230e 100644 --- a/module/services/accounts/code/work.go +++ b/module/services/accounts/code/work.go @@ -1244,10 +1244,12 @@ func doWork(ctx context.Context) (Clean, error) { // DatasourceService/ReconcileSource, which reaches exactly the engines // this sweep reaches. // Off when the operator says something else fires it. The switch is - // read once here and told to the service, which refuses the sweep too - // (SetDatasourceReconcileSweep): the tick and the sweep are one fact, - // so a host that only dropped the tick would still sweep from any - // other caller and would still describe a sweep that is not running. + // read once at startup and told to the service; the refusal lives in + // RunDatasourceReconcile (SetDatasourceReconcileSweep), so this + // callback still runs on every tick and finds a sweep that reads + // nothing. Guarding the sweep rather than the ticker is deliberate: a + // host that only dropped the tick would still sweep from any other + // caller. sweepReconcile := func() { if n, err := service.RunDatasourceReconcile(retentionCtx); err != nil { rw.Warn("datasource reconcile sweep failed", wool.ErrField(err)) diff --git a/module/services/accounts/proto/saas/accounts/v1/datasource.proto b/module/services/accounts/proto/saas/accounts/v1/datasource.proto index 058fbcac2..d64bd7cc9 100644 --- a/module/services/accounts/proto/saas/accounts/v1/datasource.proto +++ b/module/services/accounts/proto/saas/accounts/v1/datasource.proto @@ -1034,15 +1034,23 @@ message RevokeSourceDelegationResponse { // the capability that admitted it and revoking the delegation refuses the // next occurrence. // -// IT IS SET ONLY FOR A DELEGATION THE EXECUTING MODULE CAN USE. A mint by -// source id resolves the active delegation of the CALLING module, and the -// capability is addressed to the binding's declared audience, which the -// executing module must itself admit — so the one shape a declared -// reference can serve is a delegation whose binding names its own module -// as the audience. A delegation held by one module and addressed to -// another is reachable only through the delegation-id exchange, which no -// reference this host declares can name, and declaring it would read as -// bound while every occurrence was refused at the mint. +// IT IS SET ONLY FOR A DELEGATION SOME DECLARED EXECUTOR COULD USE, which +// is weaker than naming the one that will. A mint by source id resolves +// the active delegation of the CALLING module, and the capability is +// addressed to the binding's declared audience, which the executing module +// must itself admit — so the one shape a declared reference can serve is a +// delegation whose binding names its own module as the audience. A +// delegation held by one module and addressed to another is reachable only +// through the delegation-id exchange, which no reference this host +// declares can name. +// +// WHICH module that is, this host does not know: nothing it holds declares +// who executes declared work. `identity_holder` on the declaration read +// reports whose delegation the reference resolves to, and executor +// readiness is deliberately not claimed — a composed executor with no +// delegation on this source is refused at the mint +// (ErrSourceDelegationMissing), and a host that called the source "bound" +// would have been telling an operator the opposite. // // IT IS NOT OPAQUE, and revocation is therefore not permanent. The value // is the source's own id, and a mint resolves whichever delegation of that @@ -1207,6 +1215,19 @@ message SourceTriggerDeclaration { // held off its schedule: then there is no pass this host will take. bool host_sweep_enabled = 9; google.protobuf.Timestamp host_sweep_due_at = 10; + // identity_holder is the module whose delegation the reference arm resolved + // to when this declaration was built. The arm cannot carry it — the + // reference value is the source id — so it is reported beside it. + // + // IT IS NOT THE EXECUTING MODULE. Nothing this host holds declares which + // module executes declared work: a self-addressed delegation binding is + // generic and a deployment may have several. So this says "a reference on + // this source would resolve to module A's self-addressed delegation", and + // whether A is the module that installs this release is unverified here. A + // reader that took it for executor readiness would be told a source was + // ready while every occurrence of another executor's trigger was refused at + // the mint. Empty when the arm names no identity. + string identity_holder = 11; } message DelegateSourceTriggersRequest { diff --git a/module/services/frontend/code/frontend-plugin-public-api.json b/module/services/frontend/code/frontend-plugin-public-api.json index 8190a7aab..4cf8acf1e 100644 --- a/module/services/frontend/code/frontend-plugin-public-api.json +++ b/module/services/frontend/code/frontend-plugin-public-api.json @@ -124,7 +124,7 @@ }, "@codefly-dev/saas-plugin-react": { "status": "active", - "packageVersion": "0.4.5", + "packageVersion": "0.4.6", "entrypoints": [".", "./runtime", "./ui"], "entrypointExports": { ".": [ diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 719061c08..ebebe679a 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:49b6defafbf8aa3ad53bb675fd165c1896a51113da9d30edc0364fc34a7673bf", + "digest": "sha256:c57b6743854c84738f0f1454e6c93349eced569d0351a91a62786ba08c1bc806", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index c7bc3db16ebdf9478699d4def77e8a66997bff99..70814cf1f0777d58cd32b767ba3d27fd4653ca82 100644 GIT binary patch delta 20350 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 delta 18898 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 diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index 717e1d4c1..54871ebbf 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:49b6defafbf8aa3ad53bb675fd165c1896a51113da9d30edc0364fc34a7673bf + contract-digest: sha256:c57b6743854c84738f0f1454e6c93349eced569d0351a91a62786ba08c1bc806 services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/datasource_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/datasource_pb.ts index c124aa793..7d6b14af0 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/datasource_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/datasource_pb.ts @@ -18,7 +18,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/datasource.proto. */ export const file_saas_accounts_v1_datasource: GenFile = /*@__PURE__*/ - fileDesc("CiFzYWFzL2FjY291bnRzL3YxL2RhdGFzb3VyY2UucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoWR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIMCgRyZXBvGAEgASgJEg0KBXBhdGhzGAIgAygJEg4KBmJyYW5jaBgDIAEoCRIXCg9maWxlX2V4dGVuc2lvbnMYBCADKAki9AEKD0FwaU9BdXRoMkNvbmZpZxI2CgVncmFudBgEIAEoDjInLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnLkdyYW50EhEKCXRva2VuX3VybBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSDgoGc2NvcGVzGAMgAygJInMKBUdyYW50EhUKEUdSQU5UX1VOU1BFQ0lGSUVEEAASFwoTR1JBTlRfUkVGUkVTSF9UT0tFThABEhwKGEdSQU5UX0NMSUVOVF9DUkVERU5USUFMUxACEhwKGEdSQU5UX0FVVEhPUklaQVRJT05fQ09ERRADIuoBChNBcGlEYXRhc291cmNlQ29uZmlnEhAKCGJhc2VfdXJsGAEgASgJEhUKDXJlc291cmNlX3BhdGgYAiABKAkSPAoPY3JlZGVudGlhbF9raW5kGAMgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5BcGlDcmVkZW50aWFsS2luZBIZChFjcmVkZW50aWFsX2hlYWRlchgEIAEoCRIeChZjcmVkZW50aWFsX3F1ZXJ5X3BhcmFtGAUgASgJEjEKBm9hdXRoMhgGIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnIkEKF0NyYXdsZXJEYXRhc291cmNlQ29uZmlnEhMKC3NpdGVtYXBfdXJsGAEgASgJEhEKCW1heF9wYWdlcxgCIAEoDSKGAQoWVXBsb2FkRGF0YXNvdXJjZUNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIOCgZyZWdpb24YAiABKAkSDgoGYnVja2V0GAMgASgJEg4KBnByZWZpeBgEIAEoCRIVCg1hY2Nlc3Nfa2V5X2lkGAUgASgJEhMKC21heF9vYmplY3RzGAYgASgNIoMHCgpEYXRhc291cmNlEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRI2Cghwcm92aWRlchgDIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEjgKBmdpdGh1YhgFIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIyCgZzdGF0dXMYBiABKA4yIi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTdGF0dXMSGgoSd2ViaG9va19jb25maWd1cmVkGAcgASgIEi4KCmNyZWF0ZWRfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDmxhc3Rfc3luY2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCgNhcGkYCyABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWcSOgoHY3Jhd2xlchgMIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuQ3Jhd2xlckRhdGFzb3VyY2VDb25maWcSOAoGdXBsb2FkGA0gASgLMiguc2Fhcy5hY2NvdW50cy52MS5VcGxvYWREYXRhc291cmNlQ29uZmlnEhgKEGJvdW5kYXJ5X25vZGVfaWQYDiABKAkSNAoQbGFzdF9pbmdlc3RlZF9hdBgPIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoUbGFzdF9pbmdlc3RlZF9jb21taXQYECABKAkSFQoNc3RhdHVzX3JlYXNvbhgRIAEoCRIWCg5ib3VuZGFyeV9sYWJlbBgSIAEoCRISCgpjb25mb3JtYW50GBMgASgIEhcKD2NvbmZvcm1hbmNlX2dhcBgUIAEoCRI/Cg1saXZlX2RlbGl2ZXJ5GBUgASgOMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlTGl2ZURlbGl2ZXJ5EjUKEnJlY29uY2lsZV9pbnRlcnZhbBgWIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkoECAQQBVIRdGFyZ2V0X2NvbGxlY3Rpb24isgMKFkFkZEdpdEh1YlNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI7CgRyZXBvGAIgASgJQi26SCpyKBADGP8BMiFeW0EtWmEtejAtOV8uLV0rL1tBLVphLXowLTlfLi1dKyQSIAoFcGF0aHMYAyADKAlCEbpIDpIBCxBAIgdyBRABGIAEEhgKBmJyYW5jaBgEIAEoCUIIukgFcgMY/wESJAoQYm91bmRhcnlfbm9kZV9pZBgIIAEoCUIIukgFcgOwAQFIABImChBjb2xsZWN0aW9uX2xhYmVsGAkgASgJQgq6SAdyBRABGP8BSAASHgoMYWNjZXNzX3Rva2VuGAYgASgJQgi6SAVyAxiACBIgCg53ZWJob29rX3NlY3JldBgHIAEoCUIIukgFcgMYgAgSSQoPZmlsZV9leHRlbnNpb25zGAogAygJQjC6SC2SASoQICImciQQAhghMh5eXC5bQS1aYS16MC05XVtBLVphLXowLTkuXy1dKiRCEQoIYm91bmRhcnkSBbpIAggBSgQIBRAGUhF0YXJnZXRfY29sbGVjdGlvbiJLChdBZGRHaXRIdWJTb3VyY2VSZXNwb25zZRIwCgpkYXRhc291cmNlGAEgASgLMhwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlIsgEChBBZGRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESQgoIcHJvdmlkZXIYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckIKukgHggEEEAEgABI6CgZnaXRodWIYBCABKAsyKC5zYWFzLmFjY291bnRzLnYxLkdpdEh1YkRhdGFzb3VyY2VDb25maWdIABI0CgNhcGkYBSABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWdIABI8CgdjcmF3bGVyGAggASgLMikuc2Fhcy5hY2NvdW50cy52MS5DcmF3bGVyRGF0YXNvdXJjZUNvbmZpZ0gAEjoKBnVwbG9hZBgJIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuVXBsb2FkRGF0YXNvdXJjZUNvbmZpZ0gAEiQKEGJvdW5kYXJ5X25vZGVfaWQYDCABKAlCCLpIBXIDsAEBSAESJgoQY29sbGVjdGlvbl9sYWJlbBgNIAEoCUIKukgHcgUQARj/AUgBEhwKCmNyZWRlbnRpYWwYBiABKAlCCLpIBXIDGIAgEiAKDndlYmhvb2tfc2VjcmV0GAcgASgJQgi6SAVyAxiACBImChRvYXV0aDJfY2xpZW50X3NlY3JldBgKIAEoCUIIukgFcgMYgAhCCAoGY29uZmlnQhEKCGJvdW5kYXJ5EgW6SAIIAUoECAMQBFIRdGFyZ2V0X2NvbGxlY3Rpb24iRQoRQWRkU291cmNlUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJaChVEYXRhc291cmNlQ29uZmlnRmllbGQSCwoDa2V5GAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIMCgRoZWxwGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIsQBChlEYXRhc291cmNlQ29ubmVjdG9yQnVkZ2V0EhoKEm1heF9pdGVtc19wZXJfY2FsbBgBIAEoDRIaChJtYXhfYnl0ZXNfcGVyX2NhbGwYAiABKAMSFgoObWF4X2l0ZW1fYnl0ZXMYAyABKAMSHQoVb3BlcmF0aW9uc19wZXJfd2luZG93GAQgASgNEhYKDndpbmRvd19zZWNvbmRzGAUgASgDEiAKGGJhY2tncm91bmRfc2hhcmVfcGVyY2VudBgGIAEoDSKqBgocRGF0YXNvdXJjZVByb3ZpZGVyRGVzY3JpcHRvchI2Cghwcm92aWRlchgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRI+Cg1jb25maWdfZmllbGRzGAQgAygLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ29uZmlnRmllbGQSGAoQc3VwcG9ydHNfd2ViaG9vaxgFIAEoCBJHChpzdXBwb3J0ZWRfY3JlZGVudGlhbF9raW5kcxgGIAMoDjIjLnNhYXMuYWNjb3VudHMudjEuQXBpQ3JlZGVudGlhbEtpbmQSEQoJY29ubmVjdG9yGAcgASgJEkEKCWludGVyZmFjZRgIIAEoDjIuLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRJEChBjcmVkZW50aWFsX21vZGVzGAkgAygOMiouc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSPwoNcmVhZGVyc19tb2RlbBgKIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVJlYWRlcnNNb2RlbBI7CgZidWRnZXQYCyABKAsyKy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VDb25uZWN0b3JCdWRnZXQSEgoKY29uZm9ybWFudBgMIAEoCBIXCg9jb25mb3JtYW5jZV9nYXAYDSABKAkSGwoTYWNjZXB0c19uZXdfc291cmNlcxgOIAEoCBIgChhsaXZlX2RlbGl2ZXJ5X2NvbmZpZ3VyZWQYDyABKAgSGgoSYWNjZXB0c19vcGVyYXRpb25zGBAgASgIEhYKDm9wZXJhdGlvbnNfZ2FwGBEgASgJEhsKE29hdXRoX2F1dGhvcml6ZV91cmwYEiABKAkSFwoPb2F1dGhfdG9rZW5fdXJsGBMgASgJEhQKDG9hdXRoX3Njb3BlcxgUIAMoCSIdChtHZXREYXRhc291cmNlQ2F0YWxvZ1JlcXVlc3QiYQocR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZRJBCglwcm92aWRlcnMYASADKAsyLi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckRlc2NyaXB0b3IiLgoSTGlzdFNvdXJjZXNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiSAoTTGlzdFNvdXJjZXNSZXNwb25zZRIxCgtkYXRhc291cmNlcxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJCChBHZXRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIkUKEUdldFNvdXJjZVJlc3BvbnNlEjAKCmRhdGFzb3VyY2UYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2UidgoRU3luY1NvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQESHgoMYWNjZXNzX3Rva2VuGAMgASgJQgi6SAVyAxiAIBIRCglyZWNvbm5lY3QYBCABKAgiJAoSU3luY1NvdXJjZVJlc3BvbnNlEg4KBmpvYl9pZBgBIAEoCSJqChRHZXRTb3VyY2VTeW5jUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoGam9iX2lkGAMgASgJQgu6SAjYAQFyA7ABASKVAQoSU291cmNlU3luY0RlbGl2ZXJ5EhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESLQoFc3RhdGUYAiABKA4yFi5zYWFzLmpvYnMudjEuSm9iU3RhdGVCBrpIA8gBARI2CglleGVjdXRpb24YAyABKAsyIy5zYWFzLmpvYnMudjEuSm9iRXhlY3V0aW9uUmVmZXJlbmNlIosBChFTb3VyY2VTeW5jQ2hhbmdlcxINCgVmaWxlcxgBIAEoDRINCgVhZGRlZBgCIAEoDRIQCghtb2RpZmllZBgDIAEoDRIPCgdkZWxldGVkGAQgASgNEhMKC3NwbGl0X2tub3duGAUgASgIEhAKCHNuYXBzaG90GAYgASgIEg4KBmNvbW1pdBgHIAEoCSKtAQoRU291cmNlU3luY0ZhaWx1cmUSOQoGcmVhc29uGAEgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jRmFpbHVyZVJlYXNvbhIMCgRjb2RlGAIgASgJEg8KB21lc3NhZ2UYAyABKAkSEAoIcmV0cnlpbmcYBCABKAgSLAoIcmV0cnlfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoQEChJTb3VyY2VTeW5jUHJvZ3Jlc3MSMAoFcGhhc2UYASABKA4yIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNQaGFzZRI0Cgd0cmlnZ2VyGAIgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jVHJpZ2dlchItCglxdWV1ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2ZldGNoaW5nX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtjb21waWxlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMQoNaGFuZGVkX29mZl9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZmluaXNoZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjQKB2NoYW5nZXMYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNDaGFuZ2VzEjQKB2ZhaWx1cmUYCSABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNGYWlsdXJlEg8KB2F0dGVtcHQYCiABKA0SFAoMbWF4X2F0dGVtcHRzGAsgASgNItIBChVHZXRTb3VyY2VTeW5jUmVzcG9uc2USGAoGam9iX2lkGAEgASgJQgi6SAVyA7ABARItCgVzdGF0ZRgCIAEoDjIWLnNhYXMuam9icy52MS5Kb2JTdGF0ZUIGukgDyAEBEjgKCmRlbGl2ZXJpZXMYAyADKAsyJC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNEZWxpdmVyeRI2Cghwcm9ncmVzcxgEIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1Byb2dyZXNzIkUKE0RlbGV0ZVNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiFgoURGVsZXRlU291cmNlUmVzcG9uc2UiVgoTR2l0SHViQXBwUmVwb3NpdG9yeRIMCgRyZXBvGAEgASgJEhYKDmRlZmF1bHRfYnJhbmNoGAIgASgJEhkKEWFscmVhZHlfY29ubmVjdGVkGAMgASgIIjYKGkJlZ2luR2l0SHViQXBwU2V0dXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEicQobQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlEhMKC2luc3RhbGxfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIpwBCh1Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGP8BEiwKD2luc3RhbGxhdGlvbl9pZBgDIAEoCUITukgQcg4QARggMgheWzAtOV0rJBIYCgRjb2RlGAQgASgJQgq6SAdyBRABGP8BInYKHkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSOwoMcmVwb3NpdG9yaWVzGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5HaXRIdWJBcHBSZXBvc2l0b3J5IlEKH01pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiVAogTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSLEAQoVRGF0YXNvdXJjZUFjY291bnRMaW5rEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRIPCgd1c2VyX2lkGAMgASgJEhEKCWNvbm5lY3RvchgEIAEoCRIbChNwcm92aWRlcl9hY2NvdW50X2lkGAUgASgJEh4KFnByb3ZpZGVyX2FjY291bnRfbG9naW4YBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAitwEKFkRhdGFzb3VyY2VHcm91cEJpbmRpbmcSCgoCaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhEKCWNvbm5lY3RvchgDIAEoCRIZChFwcm92aWRlcl9ncm91cF9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJEhIKCmNyZWF0ZWRfYnkYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilAIKGERhdGFzb3VyY2VWZXJpZmllZERvbWFpbhIKCgJpZBgBIAEoCRIOCgZvcmdfaWQYAiABKAkSDgoGZG9tYWluGAMgASgJEjgKBnN0YXR1cxgEIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURvbWFpblN0YXR1cxIXCg90eHRfcmVjb3JkX25hbWUYBSABKAkSGAoQdHh0X3JlY29yZF92YWx1ZRgGIAEoCRIvCgt2ZXJpZmllZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKY3JlYXRlZF9hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiMwoXRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCSKmAQohQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSHgoMcmVkaXJlY3RfdXJpGAMgASgJQgi6SAVyAxiAEBIRCglzb3VyY2VfaWQYBCABKAkiegoiQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRIVCg1hdXRob3JpemVfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInUKJENvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGIBAEhgKBGNvZGUYAyABKAlCCrpIB3IFEAEY/wEiXgolQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRI1CgRsaW5rGAEgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQWNjb3VudExpbmsiPwojTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJeCiRMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2USNgoFbGlua3MYASADKAsyJy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VBY2NvdW50TGluayJUCiJEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIiUKI0RlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIjkKHUdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiiwIKHkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXNwb25zZRI2CgVsaW5rcxgBIAMoCzInLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUFjY291bnRMaW5rEjoKCGJpbmRpbmdzGAIgAygLMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlR3JvdXBCaW5kaW5nEjsKB2RvbWFpbnMYAyADKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbhI4CgV0ZWFtcxgEIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0irgEKGkJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSJQoRcHJvdmlkZXJfZ3JvdXBfaWQYAyABKAlCCrpIB3IFEAEY/wESGQoHdGVhbV9pZBgEIAEoCUIIukgFcgOwAQEiWAobQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlEjkKB2JpbmRpbmcYASABKAsyKC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VHcm91cEJpbmRpbmciTgocVW5iaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIfCh1VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJUChxDbGFpbURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGgoGZG9tYWluGAIgASgJQgq6SAdyBRADGP0BIlsKHUNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlEjoKBmRvbWFpbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVZlcmlmaWVkRG9tYWluIk8KHVZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlwKHlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXNwb25zZRI6CgZkb21haW4YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbiJPCh1EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIgCh5EZWxldGVEYXRhc291cmNlRG9tYWluUmVzcG9uc2UingIKEFNvdXJjZURlbGVnYXRpb24SCgoCaWQYASABKAkSEQoJc291cmNlX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIOCgZtb2R1bGUYBCABKAkSDwoHYmluZGluZxgFIAEoCRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpyZXZva2VkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBJACgpyZXZvY2F0aW9uGAggASgOMiwuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhISCgpyZXZva2VkX2J5GAkgASgJIoEBChxMaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIAoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABAUgAiAEBEhcKD2luY2x1ZGVfcmV2b2tlZBgDIAEoCEIMCgpfc291cmNlX2lkIlgKHUxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlEjcKC2RlbGVnYXRpb25zGAEgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIk8KHVJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlgKHlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZRI2CgpkZWxlZ2F0aW9uGAEgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIjIKD1RyaWdnZXJJZGVudGl0eRIOCgZzb3VyY2UYASABKAkSDwoHYmluZGluZxgCIAEoCSKEAQoXRGVjbGFyZWRUcmlnZ2VyU2NoZWR1bGUSDAoEY3JvbhgBIAEoCRIQCgh0aW1lem9uZRgCIAEoCRISCgptaXNzZWRfcnVuGAMgASgJEg8KB292ZXJsYXAYBCABKAkSEAoIZGVhZGxpbmUYBSABKAMSEgoKaW5wdXRfanNvbhgGIAEoCSLnAgoaVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USEQoJcmVmZXJlbmNlGAEgASgJEhUKDWRlbGVnYXRpb25faWQYAiABKAkSDgoGbW9kdWxlGAMgASgJEg8KB2JpbmRpbmcYBCABKAkSEAoIYXVkaWVuY2UYBSABKAkSDgoGdGVuYW50GAYgASgJEhoKEm93bmVyX3ByaW5jaXBhbF9pZBgHIAEoCRIOCgZzY29wZXMYCCADKAkSDgoGYWN0aXZlGAkgASgIEi4KCmNyZWF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnJldm9rZWRfYXQYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEkAKCnJldm9jYXRpb24YDCABKA4yLC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uIr4BChNUcmlnZ2VyQ2hpbGRDb250ZXh0EhUKDWRlbGVnYXRpb25faWQYASABKAkSDgoGbW9kdWxlGAIgASgJEg8KB2JpbmRpbmcYAyABKAkSEAoIYXVkaWVuY2UYBCABKAkSDgoGc2NvcGVzGAUgAygJEg4KBmxvb2t1cBgGIAEoCBItCgltaW50ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBnJlY29yZBgIIAEoCSKgAwoYU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEg8KB3JlbGVhc2UYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEjsKCHNjaGVkdWxlGAMgASgLMikuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlZFRyaWdnZXJTY2hlZHVsZRIzCghpZGVudGl0eRgEIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlcklkZW50aXR5Eg4KBmRpZ2VzdBgFIAEoCRI2ChJsYXN0X29jY3VycmVuY2VfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjYKEm5leHRfb2NjdXJyZW5jZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTdW5kZWNsYXJhYmxlX3JlYXNvbhgIIAEoCRIaChJob3N0X3N3ZWVwX2VuYWJsZWQYCSABKAgSNQoRaG9zdF9zd2VlcF9kdWVfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIlYKHURlbGVnYXRlU291cmNlVHJpZ2dlcnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABASJjCh5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVzcG9uc2USQQoLZGVsZWdhdGlvbnMYASADKAsyLC5zYWFzLmFjY291bnRzLnYxLlRyaWdnZXJEZWxlZ2F0aW9uUmVmZXJlbmNlIlsKIkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBIuIBCiNHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXNwb25zZRI/CgtkZWNsYXJhdGlvbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEkEKC2RlbGVnYXRpb25zGAIgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5UcmlnZ2VyRGVsZWdhdGlvblJlZmVyZW5jZRI3CghjaGlsZHJlbhgDIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlckNoaWxkQ29udGV4dCJuChZSZWNvbmNpbGVTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABARIdCgllZmZlY3RfaWQYAyABKAlCCrpIB3IFEAEYgAEiTAoXUmVjb25jaWxlU291cmNlUmVzcG9uc2USEQoJZWZmZWN0X2lkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIOCgZqb2JfaWQYAyABKAkiPQocTG9va3VwUmVjb25jaWxlU291cmNlUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEYgAEi8QIKD1NvdXJjZU9wZXJhdGlvbhIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg4KBm1ldGhvZBgDIAEoCRIMCgRwYXRoGAQgASgJEg0KBXF1ZXJ5GAUgAygJEi0KDGlucHV0X3NjaGVtYRgGIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSLgoNb3V0cHV0X3NjaGVtYRgHIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSOAoGZWZmZWN0GAggASgOMiguc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VPcGVyYXRpb24uRWZmZWN0EhgKEG1heF9vdXRwdXRfYnl0ZXMYCSABKA0SDgoGZGlnZXN0GAogASgJIksKBkVmZmVjdBIWChJFRkZFQ1RfVU5TUEVDSUZJRUQQABIUChBFRkZFQ1RfUkVBRF9PTkxZEAESEwoPRUZGRUNUX01VVEFUSU9OEAIijgEKHkRlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESNQoKb3BlcmF0aW9ucxgDIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIlgKH0RlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2USNQoKb3BlcmF0aW9ucxgBIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIlQKG0xpc3RTb3VyY2VPcGVyYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQEiVQocTGlzdFNvdXJjZU9wZXJhdGlvbnNSZXNwb25zZRI1CgpvcGVyYXRpb25zGAEgAygLMiEuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VPcGVyYXRpb24iwwEKHEludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEjAKCW9wZXJhdGlvbhgDIAEoCUIdukgachgyFl5bYS16XVthLXowLTlfXXswLDYzfSQSHQoKaW5wdXRfanNvbhgEIAEoCUIJukgGcgQogIAEEhsKCWVmZmVjdF9pZBgFIAEoCUIIukgFcgMogAEifwoWU291cmNlT3BlcmF0aW9uUmVjZWlwdBIRCgllZmZlY3RfaWQYASABKAkSFAoMY29tbWl0dGVkX2F0GAIgASgJEg4KBnN0YXR1cxgDIAEoCRIXCg9wcm92aWRlcl9zdGF0dXMYBCABKA0SEwoLb3V0cHV0X2pzb24YBSABKAkibwodSW52b2tlU291cmNlT3BlcmF0aW9uUmVzcG9uc2USEwoLb3V0cHV0X2pzb24YASABKAkSOQoHcmVjZWlwdBgCIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uUmVjZWlwdCJDCiJMb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0Eh0KCWVmZmVjdF9pZBgBIAEoCUIKukgHcgUQASiAASJ5CiNQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoJZWZmZWN0X2lkGAMgASgJQgi6SAVyAyiAASJbCiRQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVzcG9uc2USEQoJZWZmZWN0X2lkGAEgASgJEhAKCHJlY2VpcHRzGAIgASgDEg4KBnN0YXR1cxgDIAEoCSJKCilMb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEogAEqtwEKEkRhdGFzb3VyY2VQcm92aWRlchIjCh9EQVRBU09VUkNFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASHgoaREFUQVNPVVJDRV9QUk9WSURFUl9HSVRIVUIQARIbChdEQVRBU09VUkNFX1BST1ZJREVSX0FQSRACEh8KG0RBVEFTT1VSQ0VfUFJPVklERVJfQ1JBV0xFUhADEh4KGkRBVEFTT1VSQ0VfUFJPVklERVJfVVBMT0FEEAQqkQEKEERhdGFzb3VyY2VTdGF0dXMSIQodREFUQVNPVVJDRV9TVEFUVVNfVU5TUEVDSUZJRUQQABIcChhEQVRBU09VUkNFX1NUQVRVU19BQ1RJVkUQARIcChhEQVRBU09VUkNFX1NUQVRVU19QQVVTRUQQAhIeChpEQVRBU09VUkNFX1NUQVRVU19ERUdSQURFRBADKrwBChZEYXRhc291cmNlTGl2ZURlbGl2ZXJ5EigKJERBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEiEKHURBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9OT05FEAESKwonREFUQVNPVVJDRV9MSVZFX0RFTElWRVJZX1NPVVJDRV9XRUJIT09LEAISKAokREFUQVNPVVJDRV9MSVZFX0RFTElWRVJZX0FQUF9XRUJIT09LEAMq1gEKEUFwaUNyZWRlbnRpYWxLaW5kEiMKH0FQSV9DUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIeChpBUElfQ1JFREVOVElBTF9LSU5EX0JFQVJFUhABEh0KGUFQSV9DUkVERU5USUFMX0tJTkRfQkFTSUMQAhIeChpBUElfQ1JFREVOVElBTF9LSU5EX0hFQURFUhADEh0KGUFQSV9DUkVERU5USUFMX0tJTkRfUVVFUlkQBBIeChpBUElfQ1JFREVOVElBTF9LSU5EX09BVVRIMhAFKqYCChxEYXRhc291cmNlQ29ubmVjdG9ySW50ZXJmYWNlEi4KKkRBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9VTlNQRUNJRklFRBAAEigKJERBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9GSUxFUxABEigKJERBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9QQUdFUxACEioKJkRBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9SRUNPUkRTEAMSKwonREFUQVNPVVJDRV9DT05ORUNUT1JfSU5URVJGQUNFX01FU1NBR0VTEAQSKQolREFUQVNPVVJDRV9DT05ORUNUT1JfSU5URVJGQUNFX0VWRU5UUxAFKuwBChhEYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSKgomREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfVU5TUEVDSUZJRUQQABIjCh9EQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9OT05FEAESJgoiREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfT1JHX0FQUBACEikKJURBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX1VTRVJfT0FVVEgQAxIsCihEQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9TVEFUSUNfU0VDUkVUEAQqlwEKFkRhdGFzb3VyY2VSZWFkZXJzTW9kZWwSKAokREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1VOU1BFQ0lGSUVEEAASKgomREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1NPVVJDRV9TQ09QRUQQARInCiNEQVRBU09VUkNFX1JFQURFUlNfTU9ERUxfVFJBTlNMQVRFRBACKu4BCg9Tb3VyY2VTeW5jUGhhc2USIQodU09VUkNFX1NZTkNfUEhBU0VfVU5TUEVDSUZJRUQQABIcChhTT1VSQ0VfU1lOQ19QSEFTRV9RVUVVRUQQARIeChpTT1VSQ0VfU1lOQ19QSEFTRV9GRVRDSElORxACEh4KGlNPVVJDRV9TWU5DX1BIQVNFX0NPTVBJTEVEEAMSIAocU09VUkNFX1NZTkNfUEhBU0VfSEFOREVEX09GRhAEEhoKFlNPVVJDRV9TWU5DX1BIQVNFX0RPTkUQBRIcChhTT1VSQ0VfU1lOQ19QSEFTRV9GQUlMRUQQBiqmAwoXU291cmNlU3luY0ZhaWx1cmVSZWFzb24SKgomU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fVU5TUEVDSUZJRUQQABIrCidTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9SQVRFX0xJTUlURUQQARIpCiVTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9DUkVERU5USUFMEAISLAooU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fQUNDRVNTX0RFTklFRBADEigKJFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX05PVF9GT1VORBAEEigKJFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX1RPT19MQVJHRRAFEi8KK1NPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0hPU1RfVU5BVkFJTEFCTEUQBhIkCiBTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9PVEhFUhAHEi4KKlNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0RFTElWRVJZX0ZBSUxFRBAIKpwBChFTb3VyY2VTeW5jVHJpZ2dlchIjCh9TT1VSQ0VfU1lOQ19UUklHR0VSX1VOU1BFQ0lGSUVEEAASHgoaU09VUkNFX1NZTkNfVFJJR0dFUl9NQU5VQUwQARIhCh1TT1VSQ0VfU1lOQ19UUklHR0VSX1NDSEVEVUxFRBACEh8KG1NPVVJDRV9TWU5DX1RSSUdHRVJfV0VCSE9PSxADKo8BChZEYXRhc291cmNlRG9tYWluU3RhdHVzEigKJERBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19VTlNQRUNJRklFRBAAEiQKIERBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19QRU5ESU5HEAESJQohREFUQVNPVVJDRV9ET01BSU5fU1RBVFVTX1ZFUklGSUVEEAIqlQMKGlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uEiwKKFNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fVU5TUEVDSUZJRUQQABIoCiRTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1JFVk9LRUQQARIpCiVTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1JFUExBQ0VEEAISLworU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9TT1VSQ0VfREVMRVRFRBADEi8KK1NPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fTUVNQkVSX1JFTU9WRUQQBBIwCixTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1BFUk1JU1NJT05fTE9TVBAFEi4KKlNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fVVNFUl9JTkFDVElWRRAGEjAKLFNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fQklORElOR19DSEFOR0VEEAcyiTgKEURhdGFzb3VyY2VTZXJ2aWNlErkDChxQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzEjUuc2Fhcy5hY2NvdW50cy52MS5QcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlIqkCwvMYbAgCEAMaEWRhdGFzb3VyY2U6aW52b2tlIhFkYXRhc291cmNlOmludm9rZSoPCglzb3VyY2VfaWQQBBgDMAE6Iwofc2Fhcy5kYXRhc291cmNlLnJlY2VpcHRzLnBydW5lZBACQANIBFADWANgAdrzGLQBCgIIPBIDCNgEGAUiAggCKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMg9zYWFzLWRhdGFzb3VyY2VKRi9zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTZXJ2aWNlL0xvb2t1cFBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNQgCBYgCBgAXIYCgZzb3VyY2USBmludm9rZRIEcmVhZBgBEuYBCiJMb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzEjsuc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlIkvC8xhHCAIQAxoPZGF0YXNvdXJjZTpyZWFkIg9kYXRhc291cmNlOnJlYWQqDwoJZWZmZWN0X2lkEAQYAzABOgIQAUABSANQA1gDYAES/AMKFUludm9rZVNvdXJjZU9wZXJhdGlvbhIuLnNhYXMuYWNjb3VudHMudjEuSW52b2tlU291cmNlT3BlcmF0aW9uUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuSW52b2tlU291cmNlT3BlcmF0aW9uUmVzcG9uc2UigQPC8xhuCAIQAxoRZGF0YXNvdXJjZTppbnZva2UiEWRhdGFzb3VyY2U6aW52b2tlKg8KCXNvdXJjZV9pZBAEGAMwATolCiFzYWFzLmRhdGFzb3VyY2Uub3BlcmF0aW9uLmludm9rZWQQBEADSARQBFgEYAHa8xivAQoCCDwSAwjYBBgFIgIIAioLVU5BVkFJTEFCTEUqElJFU09VUkNFX0VYSEFVU1RFRCoHQUJPUlRFRDIPc2Fhcy1kYXRhc291cmNlSj8vc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlU2VydmljZS9Mb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25QgIAEWICAQGABchgKBnNvdXJjZRIGaW52b2tlEgRyZWFkGAGC0+STAlU6ASoiUC92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMve29wZXJhdGlvbn06aW52b2tlEocCChtMb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb24SNC5zYWFzLmFjY291bnRzLnYxLkxvb2t1cEludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlIoABwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUANYBGABgtPkkwIvEi0vdjEvZGF0YXNvdXJjZS1vcGVyYXRpb24tcmVjZWlwdHMve2VmZmVjdF9pZH0ShQIKFExpc3RTb3VyY2VPcGVyYXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlT3BlcmF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2UijQHC8xhECAIQAxoPZGF0YXNvdXJjZTpyZWFkIg9kYXRhc291cmNlOnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAj8SPS92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMSkwIKF0RlY2xhcmVTb3VyY2VPcGVyYXRpb25zEjAuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1JlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkRlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2UikgHC8xhGCAIQBCoMCgZvcmdfaWQQAhgBMAE6Jgoic2Fhcy5kYXRhc291cmNlLm9wZXJhdGlvbi5kZWNsYXJlZBACQAFIBFADWANgAYLT5JMCQjoBKiI9L3YxL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZGF0YXNvdXJjZXMve3NvdXJjZV9pZH0vb3BlcmF0aW9ucxKsAQoPQWRkR2l0SHViU291cmNlEiguc2Fhcy5hY2NvdW50cy52MS5BZGRHaXRIdWJTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5BZGRHaXRIdWJTb3VyY2VSZXNwb25zZSJEwvMYQAgCEAQqDAoGb3JnX2lkEAIYATABOiAKHHNhYXMuZGF0YXNvdXJjZS5zb3VyY2UuYWRkZWQQAkABSARQBFgDYAESmgEKCUFkZFNvdXJjZRIiLnNhYXMuYWNjb3VudHMudjEuQWRkU291cmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuQWRkU291cmNlUmVzcG9uc2UiRMLzGEAIAhAEKgwKBm9yZ19pZBACGAEwATogChxzYWFzLmRhdGFzb3VyY2Uuc291cmNlLmFkZGVkEAJAAUgEUARYA2ABEo8BChRHZXREYXRhc291cmNlQ2F0YWxvZxItLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlQ2F0YWxvZ1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSANQAlgCYAESggEKC0xpc3RTb3VyY2VzEiQuc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VzUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEnwKCUdldFNvdXJjZRIiLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEp4BCgpTeW5jU291cmNlEiMuc2Fhcy5hY2NvdW50cy52MS5TeW5jU291cmNlUmVxdWVzdBokLnNhYXMuYWNjb3VudHMudjEuU3luY1NvdXJjZVJlc3BvbnNlIkXC8xhBCAIQBCoMCgZvcmdfaWQQAhgBMAE6IQodc2Fhcy5kYXRhc291cmNlLnNvdXJjZS5zeW5jZWQQAkABSARQBFgCYAESiAEKDUdldFNvdXJjZVN5bmMSJi5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVN5bmNSZXF1ZXN0Gicuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VTeW5jUmVzcG9uc2UiJsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEqUBCgxEZWxldGVTb3VyY2USJS5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZVNvdXJjZVJlcXVlc3QaJi5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZVNvdXJjZVJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLnNvdXJjZS5yZW1vdmVkEAJAAUgEUAJYAmABEsQBChNCZWdpbkdpdEh1YkFwcFNldHVwEiwuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkdpdEh1YkFwcFNldHVwUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlIlDC8xhMCAIQBCoMCgZvcmdfaWQQAhgBMAE6LAooc2Fhcy5kYXRhc291cmNlLmdpdGh1Yl9hcHAuc2V0dXBfc3RhcnRlZBACQAFIBFACWARgARLPAQoWQ29tcGxldGVHaXRIdWJBcHBTZXR1cBIvLnNhYXMuYWNjb3VudHMudjEuQ29tcGxldGVHaXRIdWJBcHBTZXR1cFJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZSJSwvMYTggCEAQqDAoGb3JnX2lkEAIYATABOi4KKnNhYXMuZGF0YXNvdXJjZS5naXRodWJfYXBwLnNldHVwX2NvbXBsZXRlZBACQAFIBFAEWANgARLNAQoYTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwEjEuc2Fhcy5hY2NvdW50cy52MS5NaWdyYXRlR2l0SHViU291cmNlVG9BcHBSZXF1ZXN0GjIuc2Fhcy5hY2NvdW50cy52MS5NaWdyYXRlR2l0SHViU291cmNlVG9BcHBSZXNwb25zZSJKwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5jcmVkZW50aWFsLnVwZGF0ZWQQAkABSARQAlgDYAES1QEKGkJlZ2luRGF0YXNvdXJjZUFjY291bnRMaW5rEjMuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkRhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNC5zYWFzLmFjY291bnRzLnYxLkJlZ2luRGF0YXNvdXJjZUFjY291bnRMaW5rUmVzcG9uc2UiTMLzGEgIAhADKgwKBm9yZ19pZBACGAEwATooCiRzYWFzLmRhdGFzb3VyY2UuYWNjb3VudF9saW5rX3N0YXJ0ZWQQAkABSARQAlgEYAES2AEKHUNvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rEjYuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVzcG9uc2UiRsLzGEIIAhADKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLmRhdGFzb3VyY2UuYWNjb3VudF9saW5rZWQQAkABSARQBFgDYAEStQEKHExpc3RNeURhdGFzb3VyY2VBY2NvdW50TGlua3MSNS5zYWFzLmFjY291bnRzLnYxLkxpc3RNeURhdGFzb3VyY2VBY2NvdW50TGlua3NSZXF1ZXN0GjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEtQBChtEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmsSNC5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkjC8xhECAIQAyoMCgZvcmdfaWQQAhgBMAE6JAogc2Fhcy5kYXRhc291cmNlLmFjY291bnRfdW5saW5rZWQQAkABSARQAlgDYAESowEKFkdldERhdGFzb3VyY2VEaXJlY3RvcnkSLy5zYWFzLmFjY291bnRzLnYxLkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlRGlyZWN0b3J5UmVzcG9uc2UiJsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABErcBChNCaW5kRGF0YXNvdXJjZUdyb3VwEiwuc2Fhcy5hY2NvdW50cy52MS5CaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlIkPC8xg/CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hwobc2Fhcy5kYXRhc291cmNlLmdyb3VwX2JvdW5kEAJAAUgEUAJYA2ABEr8BChVVbmJpbmREYXRhc291cmNlR3JvdXASLi5zYWFzLmFjY291bnRzLnYxLlVuYmluZERhdGFzb3VyY2VHcm91cFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLlVuYmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlIkXC8xhBCAIQBCoMCgZvcmdfaWQQAhgBMAE6IQodc2Fhcy5kYXRhc291cmNlLmdyb3VwX3VuYm91bmQQAkABSARQAlgDYAESwAEKFUNsYWltRGF0YXNvdXJjZURvbWFpbhIuLnNhYXMuYWNjb3VudHMudjEuQ2xhaW1EYXRhc291cmNlRG9tYWluUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuQ2xhaW1EYXRhc291cmNlRG9tYWluUmVzcG9uc2UiRsLzGEIIAhAEKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLmRhdGFzb3VyY2UuZG9tYWluX2NsYWltZWQQAkABSARQAlgDYAESxAEKFlZlcmlmeURhdGFzb3VyY2VEb21haW4SLy5zYWFzLmFjY291bnRzLnYxLlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5WZXJpZnlEYXRhc291cmNlRG9tYWluUmVzcG9uc2UiR8LzGEMIAhAEKgwKBm9yZ19pZBACGAEwATojCh9zYWFzLmRhdGFzb3VyY2UuZG9tYWluX3ZlcmlmaWVkEAJAAUgEUAJYA2ABEsMBChZEZWxldGVEYXRhc291cmNlRG9tYWluEi8uc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuRGVsZXRlRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmRvbWFpbl9yZW1vdmVkEAJAAUgEUAJYA2ABEqABChVMaXN0U291cmNlRGVsZWdhdGlvbnMSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VEZWxlZ2F0aW9uc1JlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARLHAQoWUmV2b2tlU291cmNlRGVsZWdhdGlvbhIvLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU291cmNlRGVsZWdhdGlvblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZSJKwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5kZWxlZ2F0aW9uLnJldm9rZWQQAkABSARQAlgDYAES6wEKFkRlbGVnYXRlU291cmNlVHJpZ2dlcnMSLy5zYWFzLmFjY291bnRzLnYxLkRlbGVnYXRlU291cmNlVHJpZ2dlcnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVzcG9uc2UibsLzGGoIAhAEKgwKBm9yZ19pZBACGAEwATpKCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5jcmVhdGVkCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5yZXZva2VkEAJAAUgEUAJYA2ABErIBChtHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb24SNC5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKDAwoPUmVjb25jaWxlU291cmNlEiguc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXNwb25zZSKaAsLzGGoIAhADGhFkYXRhc291cmNlOmludm9rZSIRZGF0YXNvdXJjZTppbnZva2UqDwoJc291cmNlX2lkEAQYAzABOiEKHXNhYXMuZGF0YXNvdXJjZS5zb3VyY2Uuc3luY2VkEAJAA0gEUAJYAmAB2vMYpwEKAgg8EgMI2AQYBSICCAIqC1VOQVZBSUxBQkxFKhJSRVNPVVJDRV9FWEhBVVNURUQqB0FCT1JURUQyD3NhYXMtZGF0YXNvdXJjZUo5L3NhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVNlcnZpY2UvTG9va3VwUmVjb25jaWxlU291cmNlUIAgWIAgYAFyGAoGc291cmNlEgZpbnZva2USBHJlYWQYARK/AQoVTG9va3VwUmVjb25jaWxlU291cmNlEi4uc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBSZWNvbmNpbGVTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXNwb25zZSJLwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUAJYAmABYgZwcm90bzM", [file_buf_validate_validate, file_codefly_runnable_v0_options, file_google_api_annotations, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiFzYWFzL2FjY291bnRzL3YxL2RhdGFzb3VyY2UucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoWR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIMCgRyZXBvGAEgASgJEg0KBXBhdGhzGAIgAygJEg4KBmJyYW5jaBgDIAEoCRIXCg9maWxlX2V4dGVuc2lvbnMYBCADKAki9AEKD0FwaU9BdXRoMkNvbmZpZxI2CgVncmFudBgEIAEoDjInLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnLkdyYW50EhEKCXRva2VuX3VybBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSDgoGc2NvcGVzGAMgAygJInMKBUdyYW50EhUKEUdSQU5UX1VOU1BFQ0lGSUVEEAASFwoTR1JBTlRfUkVGUkVTSF9UT0tFThABEhwKGEdSQU5UX0NMSUVOVF9DUkVERU5USUFMUxACEhwKGEdSQU5UX0FVVEhPUklaQVRJT05fQ09ERRADIuoBChNBcGlEYXRhc291cmNlQ29uZmlnEhAKCGJhc2VfdXJsGAEgASgJEhUKDXJlc291cmNlX3BhdGgYAiABKAkSPAoPY3JlZGVudGlhbF9raW5kGAMgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5BcGlDcmVkZW50aWFsS2luZBIZChFjcmVkZW50aWFsX2hlYWRlchgEIAEoCRIeChZjcmVkZW50aWFsX3F1ZXJ5X3BhcmFtGAUgASgJEjEKBm9hdXRoMhgGIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnIkEKF0NyYXdsZXJEYXRhc291cmNlQ29uZmlnEhMKC3NpdGVtYXBfdXJsGAEgASgJEhEKCW1heF9wYWdlcxgCIAEoDSKGAQoWVXBsb2FkRGF0YXNvdXJjZUNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIOCgZyZWdpb24YAiABKAkSDgoGYnVja2V0GAMgASgJEg4KBnByZWZpeBgEIAEoCRIVCg1hY2Nlc3Nfa2V5X2lkGAUgASgJEhMKC21heF9vYmplY3RzGAYgASgNIoMHCgpEYXRhc291cmNlEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRI2Cghwcm92aWRlchgDIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEjgKBmdpdGh1YhgFIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIyCgZzdGF0dXMYBiABKA4yIi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTdGF0dXMSGgoSd2ViaG9va19jb25maWd1cmVkGAcgASgIEi4KCmNyZWF0ZWRfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDmxhc3Rfc3luY2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCgNhcGkYCyABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWcSOgoHY3Jhd2xlchgMIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuQ3Jhd2xlckRhdGFzb3VyY2VDb25maWcSOAoGdXBsb2FkGA0gASgLMiguc2Fhcy5hY2NvdW50cy52MS5VcGxvYWREYXRhc291cmNlQ29uZmlnEhgKEGJvdW5kYXJ5X25vZGVfaWQYDiABKAkSNAoQbGFzdF9pbmdlc3RlZF9hdBgPIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoUbGFzdF9pbmdlc3RlZF9jb21taXQYECABKAkSFQoNc3RhdHVzX3JlYXNvbhgRIAEoCRIWCg5ib3VuZGFyeV9sYWJlbBgSIAEoCRISCgpjb25mb3JtYW50GBMgASgIEhcKD2NvbmZvcm1hbmNlX2dhcBgUIAEoCRI/Cg1saXZlX2RlbGl2ZXJ5GBUgASgOMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlTGl2ZURlbGl2ZXJ5EjUKEnJlY29uY2lsZV9pbnRlcnZhbBgWIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkoECAQQBVIRdGFyZ2V0X2NvbGxlY3Rpb24isgMKFkFkZEdpdEh1YlNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI7CgRyZXBvGAIgASgJQi26SCpyKBADGP8BMiFeW0EtWmEtejAtOV8uLV0rL1tBLVphLXowLTlfLi1dKyQSIAoFcGF0aHMYAyADKAlCEbpIDpIBCxBAIgdyBRABGIAEEhgKBmJyYW5jaBgEIAEoCUIIukgFcgMY/wESJAoQYm91bmRhcnlfbm9kZV9pZBgIIAEoCUIIukgFcgOwAQFIABImChBjb2xsZWN0aW9uX2xhYmVsGAkgASgJQgq6SAdyBRABGP8BSAASHgoMYWNjZXNzX3Rva2VuGAYgASgJQgi6SAVyAxiACBIgCg53ZWJob29rX3NlY3JldBgHIAEoCUIIukgFcgMYgAgSSQoPZmlsZV9leHRlbnNpb25zGAogAygJQjC6SC2SASoQICImciQQAhghMh5eXC5bQS1aYS16MC05XVtBLVphLXowLTkuXy1dKiRCEQoIYm91bmRhcnkSBbpIAggBSgQIBRAGUhF0YXJnZXRfY29sbGVjdGlvbiJLChdBZGRHaXRIdWJTb3VyY2VSZXNwb25zZRIwCgpkYXRhc291cmNlGAEgASgLMhwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlIsgEChBBZGRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESQgoIcHJvdmlkZXIYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckIKukgHggEEEAEgABI6CgZnaXRodWIYBCABKAsyKC5zYWFzLmFjY291bnRzLnYxLkdpdEh1YkRhdGFzb3VyY2VDb25maWdIABI0CgNhcGkYBSABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWdIABI8CgdjcmF3bGVyGAggASgLMikuc2Fhcy5hY2NvdW50cy52MS5DcmF3bGVyRGF0YXNvdXJjZUNvbmZpZ0gAEjoKBnVwbG9hZBgJIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuVXBsb2FkRGF0YXNvdXJjZUNvbmZpZ0gAEiQKEGJvdW5kYXJ5X25vZGVfaWQYDCABKAlCCLpIBXIDsAEBSAESJgoQY29sbGVjdGlvbl9sYWJlbBgNIAEoCUIKukgHcgUQARj/AUgBEhwKCmNyZWRlbnRpYWwYBiABKAlCCLpIBXIDGIAgEiAKDndlYmhvb2tfc2VjcmV0GAcgASgJQgi6SAVyAxiACBImChRvYXV0aDJfY2xpZW50X3NlY3JldBgKIAEoCUIIukgFcgMYgAhCCAoGY29uZmlnQhEKCGJvdW5kYXJ5EgW6SAIIAUoECAMQBFIRdGFyZ2V0X2NvbGxlY3Rpb24iRQoRQWRkU291cmNlUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJaChVEYXRhc291cmNlQ29uZmlnRmllbGQSCwoDa2V5GAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIMCgRoZWxwGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIsQBChlEYXRhc291cmNlQ29ubmVjdG9yQnVkZ2V0EhoKEm1heF9pdGVtc19wZXJfY2FsbBgBIAEoDRIaChJtYXhfYnl0ZXNfcGVyX2NhbGwYAiABKAMSFgoObWF4X2l0ZW1fYnl0ZXMYAyABKAMSHQoVb3BlcmF0aW9uc19wZXJfd2luZG93GAQgASgNEhYKDndpbmRvd19zZWNvbmRzGAUgASgDEiAKGGJhY2tncm91bmRfc2hhcmVfcGVyY2VudBgGIAEoDSKqBgocRGF0YXNvdXJjZVByb3ZpZGVyRGVzY3JpcHRvchI2Cghwcm92aWRlchgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRI+Cg1jb25maWdfZmllbGRzGAQgAygLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ29uZmlnRmllbGQSGAoQc3VwcG9ydHNfd2ViaG9vaxgFIAEoCBJHChpzdXBwb3J0ZWRfY3JlZGVudGlhbF9raW5kcxgGIAMoDjIjLnNhYXMuYWNjb3VudHMudjEuQXBpQ3JlZGVudGlhbEtpbmQSEQoJY29ubmVjdG9yGAcgASgJEkEKCWludGVyZmFjZRgIIAEoDjIuLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRJEChBjcmVkZW50aWFsX21vZGVzGAkgAygOMiouc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSPwoNcmVhZGVyc19tb2RlbBgKIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVJlYWRlcnNNb2RlbBI7CgZidWRnZXQYCyABKAsyKy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VDb25uZWN0b3JCdWRnZXQSEgoKY29uZm9ybWFudBgMIAEoCBIXCg9jb25mb3JtYW5jZV9nYXAYDSABKAkSGwoTYWNjZXB0c19uZXdfc291cmNlcxgOIAEoCBIgChhsaXZlX2RlbGl2ZXJ5X2NvbmZpZ3VyZWQYDyABKAgSGgoSYWNjZXB0c19vcGVyYXRpb25zGBAgASgIEhYKDm9wZXJhdGlvbnNfZ2FwGBEgASgJEhsKE29hdXRoX2F1dGhvcml6ZV91cmwYEiABKAkSFwoPb2F1dGhfdG9rZW5fdXJsGBMgASgJEhQKDG9hdXRoX3Njb3BlcxgUIAMoCSIdChtHZXREYXRhc291cmNlQ2F0YWxvZ1JlcXVlc3QiYQocR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZRJBCglwcm92aWRlcnMYASADKAsyLi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckRlc2NyaXB0b3IiLgoSTGlzdFNvdXJjZXNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiSAoTTGlzdFNvdXJjZXNSZXNwb25zZRIxCgtkYXRhc291cmNlcxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJCChBHZXRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIkUKEUdldFNvdXJjZVJlc3BvbnNlEjAKCmRhdGFzb3VyY2UYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2UidgoRU3luY1NvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQESHgoMYWNjZXNzX3Rva2VuGAMgASgJQgi6SAVyAxiAIBIRCglyZWNvbm5lY3QYBCABKAgiJAoSU3luY1NvdXJjZVJlc3BvbnNlEg4KBmpvYl9pZBgBIAEoCSJqChRHZXRTb3VyY2VTeW5jUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoGam9iX2lkGAMgASgJQgu6SAjYAQFyA7ABASKVAQoSU291cmNlU3luY0RlbGl2ZXJ5EhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESLQoFc3RhdGUYAiABKA4yFi5zYWFzLmpvYnMudjEuSm9iU3RhdGVCBrpIA8gBARI2CglleGVjdXRpb24YAyABKAsyIy5zYWFzLmpvYnMudjEuSm9iRXhlY3V0aW9uUmVmZXJlbmNlIosBChFTb3VyY2VTeW5jQ2hhbmdlcxINCgVmaWxlcxgBIAEoDRINCgVhZGRlZBgCIAEoDRIQCghtb2RpZmllZBgDIAEoDRIPCgdkZWxldGVkGAQgASgNEhMKC3NwbGl0X2tub3duGAUgASgIEhAKCHNuYXBzaG90GAYgASgIEg4KBmNvbW1pdBgHIAEoCSKtAQoRU291cmNlU3luY0ZhaWx1cmUSOQoGcmVhc29uGAEgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jRmFpbHVyZVJlYXNvbhIMCgRjb2RlGAIgASgJEg8KB21lc3NhZ2UYAyABKAkSEAoIcmV0cnlpbmcYBCABKAgSLAoIcmV0cnlfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoQEChJTb3VyY2VTeW5jUHJvZ3Jlc3MSMAoFcGhhc2UYASABKA4yIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNQaGFzZRI0Cgd0cmlnZ2VyGAIgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jVHJpZ2dlchItCglxdWV1ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2ZldGNoaW5nX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtjb21waWxlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMQoNaGFuZGVkX29mZl9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZmluaXNoZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjQKB2NoYW5nZXMYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNDaGFuZ2VzEjQKB2ZhaWx1cmUYCSABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNGYWlsdXJlEg8KB2F0dGVtcHQYCiABKA0SFAoMbWF4X2F0dGVtcHRzGAsgASgNItIBChVHZXRTb3VyY2VTeW5jUmVzcG9uc2USGAoGam9iX2lkGAEgASgJQgi6SAVyA7ABARItCgVzdGF0ZRgCIAEoDjIWLnNhYXMuam9icy52MS5Kb2JTdGF0ZUIGukgDyAEBEjgKCmRlbGl2ZXJpZXMYAyADKAsyJC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNEZWxpdmVyeRI2Cghwcm9ncmVzcxgEIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1Byb2dyZXNzIkUKE0RlbGV0ZVNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiFgoURGVsZXRlU291cmNlUmVzcG9uc2UiVgoTR2l0SHViQXBwUmVwb3NpdG9yeRIMCgRyZXBvGAEgASgJEhYKDmRlZmF1bHRfYnJhbmNoGAIgASgJEhkKEWFscmVhZHlfY29ubmVjdGVkGAMgASgIIjYKGkJlZ2luR2l0SHViQXBwU2V0dXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEicQobQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlEhMKC2luc3RhbGxfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIpwBCh1Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGP8BEiwKD2luc3RhbGxhdGlvbl9pZBgDIAEoCUITukgQcg4QARggMgheWzAtOV0rJBIYCgRjb2RlGAQgASgJQgq6SAdyBRABGP8BInYKHkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSOwoMcmVwb3NpdG9yaWVzGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5HaXRIdWJBcHBSZXBvc2l0b3J5IlEKH01pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiVAogTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSLEAQoVRGF0YXNvdXJjZUFjY291bnRMaW5rEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRIPCgd1c2VyX2lkGAMgASgJEhEKCWNvbm5lY3RvchgEIAEoCRIbChNwcm92aWRlcl9hY2NvdW50X2lkGAUgASgJEh4KFnByb3ZpZGVyX2FjY291bnRfbG9naW4YBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAitwEKFkRhdGFzb3VyY2VHcm91cEJpbmRpbmcSCgoCaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhEKCWNvbm5lY3RvchgDIAEoCRIZChFwcm92aWRlcl9ncm91cF9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJEhIKCmNyZWF0ZWRfYnkYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilAIKGERhdGFzb3VyY2VWZXJpZmllZERvbWFpbhIKCgJpZBgBIAEoCRIOCgZvcmdfaWQYAiABKAkSDgoGZG9tYWluGAMgASgJEjgKBnN0YXR1cxgEIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURvbWFpblN0YXR1cxIXCg90eHRfcmVjb3JkX25hbWUYBSABKAkSGAoQdHh0X3JlY29yZF92YWx1ZRgGIAEoCRIvCgt2ZXJpZmllZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKY3JlYXRlZF9hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiMwoXRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCSKmAQohQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSHgoMcmVkaXJlY3RfdXJpGAMgASgJQgi6SAVyAxiAEBIRCglzb3VyY2VfaWQYBCABKAkiegoiQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRIVCg1hdXRob3JpemVfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInUKJENvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGIBAEhgKBGNvZGUYAyABKAlCCrpIB3IFEAEY/wEiXgolQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRI1CgRsaW5rGAEgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQWNjb3VudExpbmsiPwojTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJeCiRMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2USNgoFbGlua3MYASADKAsyJy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VBY2NvdW50TGluayJUCiJEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIiUKI0RlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIjkKHUdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiiwIKHkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXNwb25zZRI2CgVsaW5rcxgBIAMoCzInLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUFjY291bnRMaW5rEjoKCGJpbmRpbmdzGAIgAygLMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlR3JvdXBCaW5kaW5nEjsKB2RvbWFpbnMYAyADKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbhI4CgV0ZWFtcxgEIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0irgEKGkJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSJQoRcHJvdmlkZXJfZ3JvdXBfaWQYAyABKAlCCrpIB3IFEAEY/wESGQoHdGVhbV9pZBgEIAEoCUIIukgFcgOwAQEiWAobQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlEjkKB2JpbmRpbmcYASABKAsyKC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VHcm91cEJpbmRpbmciTgocVW5iaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIfCh1VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJUChxDbGFpbURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGgoGZG9tYWluGAIgASgJQgq6SAdyBRADGP0BIlsKHUNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlEjoKBmRvbWFpbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVZlcmlmaWVkRG9tYWluIk8KHVZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlwKHlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXNwb25zZRI6CgZkb21haW4YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbiJPCh1EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIgCh5EZWxldGVEYXRhc291cmNlRG9tYWluUmVzcG9uc2UingIKEFNvdXJjZURlbGVnYXRpb24SCgoCaWQYASABKAkSEQoJc291cmNlX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIOCgZtb2R1bGUYBCABKAkSDwoHYmluZGluZxgFIAEoCRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpyZXZva2VkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBJACgpyZXZvY2F0aW9uGAggASgOMiwuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhISCgpyZXZva2VkX2J5GAkgASgJIoEBChxMaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIAoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABAUgAiAEBEhcKD2luY2x1ZGVfcmV2b2tlZBgDIAEoCEIMCgpfc291cmNlX2lkIlgKHUxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlEjcKC2RlbGVnYXRpb25zGAEgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIk8KHVJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlgKHlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZRI2CgpkZWxlZ2F0aW9uGAEgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIjIKD1RyaWdnZXJJZGVudGl0eRIOCgZzb3VyY2UYASABKAkSDwoHYmluZGluZxgCIAEoCSKEAQoXRGVjbGFyZWRUcmlnZ2VyU2NoZWR1bGUSDAoEY3JvbhgBIAEoCRIQCgh0aW1lem9uZRgCIAEoCRISCgptaXNzZWRfcnVuGAMgASgJEg8KB292ZXJsYXAYBCABKAkSEAoIZGVhZGxpbmUYBSABKAMSEgoKaW5wdXRfanNvbhgGIAEoCSLnAgoaVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USEQoJcmVmZXJlbmNlGAEgASgJEhUKDWRlbGVnYXRpb25faWQYAiABKAkSDgoGbW9kdWxlGAMgASgJEg8KB2JpbmRpbmcYBCABKAkSEAoIYXVkaWVuY2UYBSABKAkSDgoGdGVuYW50GAYgASgJEhoKEm93bmVyX3ByaW5jaXBhbF9pZBgHIAEoCRIOCgZzY29wZXMYCCADKAkSDgoGYWN0aXZlGAkgASgIEi4KCmNyZWF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnJldm9rZWRfYXQYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEkAKCnJldm9jYXRpb24YDCABKA4yLC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uIr4BChNUcmlnZ2VyQ2hpbGRDb250ZXh0EhUKDWRlbGVnYXRpb25faWQYASABKAkSDgoGbW9kdWxlGAIgASgJEg8KB2JpbmRpbmcYAyABKAkSEAoIYXVkaWVuY2UYBCABKAkSDgoGc2NvcGVzGAUgAygJEg4KBmxvb2t1cBgGIAEoCBItCgltaW50ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBnJlY29yZBgIIAEoCSK5AwoYU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEg8KB3JlbGVhc2UYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEjsKCHNjaGVkdWxlGAMgASgLMikuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlZFRyaWdnZXJTY2hlZHVsZRIzCghpZGVudGl0eRgEIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlcklkZW50aXR5Eg4KBmRpZ2VzdBgFIAEoCRI2ChJsYXN0X29jY3VycmVuY2VfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjYKEm5leHRfb2NjdXJyZW5jZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTdW5kZWNsYXJhYmxlX3JlYXNvbhgIIAEoCRIaChJob3N0X3N3ZWVwX2VuYWJsZWQYCSABKAgSNQoRaG9zdF9zd2VlcF9kdWVfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKD2lkZW50aXR5X2hvbGRlchgLIAEoCSJWCh1EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQEiYwoeRGVsZWdhdGVTb3VyY2VUcmlnZ2Vyc1Jlc3BvbnNlEkEKC2RlbGVnYXRpb25zGAEgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5UcmlnZ2VyRGVsZWdhdGlvblJlZmVyZW5jZSJbCiJHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABASLiAQojR2V0U291cmNlVHJpZ2dlckRlY2xhcmF0aW9uUmVzcG9uc2USPwoLZGVjbGFyYXRpb24YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvbhJBCgtkZWxlZ2F0aW9ucxgCIAMoCzIsLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USNwoIY2hpbGRyZW4YAyADKAsyJS5zYWFzLmFjY291bnRzLnYxLlRyaWdnZXJDaGlsZENvbnRleHQibgoWUmVjb25jaWxlU291cmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESHQoJZWZmZWN0X2lkGAMgASgJQgq6SAdyBRABGIABIkwKF1JlY29uY2lsZVNvdXJjZVJlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIOCgZzdGF0dXMYAiABKAkSDgoGam9iX2lkGAMgASgJIj0KHExvb2t1cFJlY29uY2lsZVNvdXJjZVJlcXVlc3QSHQoJZWZmZWN0X2lkGAEgASgJQgq6SAdyBRABGIABIvECCg9Tb3VyY2VPcGVyYXRpb24SDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIOCgZtZXRob2QYAyABKAkSDAoEcGF0aBgEIAEoCRINCgVxdWVyeRgFIAMoCRItCgxpbnB1dF9zY2hlbWEYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0Ei4KDW91dHB1dF9zY2hlbWEYByABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EjgKBmVmZmVjdBgIIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uLkVmZmVjdBIYChBtYXhfb3V0cHV0X2J5dGVzGAkgASgNEg4KBmRpZ2VzdBgKIAEoCSJLCgZFZmZlY3QSFgoSRUZGRUNUX1VOU1BFQ0lGSUVEEAASFAoQRUZGRUNUX1JFQURfT05MWRABEhMKD0VGRkVDVF9NVVRBVElPThACIo4BCh5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEjUKCm9wZXJhdGlvbnMYAyADKAsyIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvbiJYCh9EZWNsYXJlU291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlEjUKCm9wZXJhdGlvbnMYASADKAsyIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvbiJUChtMaXN0U291cmNlT3BlcmF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBIlUKHExpc3RTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2USNQoKb3BlcmF0aW9ucxgBIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIsMBChxJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABARIwCglvcGVyYXRpb24YAyABKAlCHbpIGnIYMhZeW2Etel1bYS16MC05X117MCw2M30kEh0KCmlucHV0X2pzb24YBCABKAlCCbpIBnIEKICABBIbCgllZmZlY3RfaWQYBSABKAlCCLpIBXIDKIABIn8KFlNvdXJjZU9wZXJhdGlvblJlY2VpcHQSEQoJZWZmZWN0X2lkGAEgASgJEhQKDGNvbW1pdHRlZF9hdBgCIAEoCRIOCgZzdGF0dXMYAyABKAkSFwoPcHJvdmlkZXJfc3RhdHVzGAQgASgNEhMKC291dHB1dF9qc29uGAUgASgJIm8KHUludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlEhMKC291dHB1dF9qc29uGAEgASgJEjkKB3JlY2VpcHQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvblJlY2VpcHQiQwoiTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEogAEieQojUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEhsKCWVmZmVjdF9pZBgDIAEoCUIIukgFcgMogAEiWwokUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIQCghyZWNlaXB0cxgCIAEoAxIOCgZzdGF0dXMYAyABKAkiSgopTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QSHQoJZWZmZWN0X2lkGAEgASgJQgq6SAdyBRABKIABKrcBChJEYXRhc291cmNlUHJvdmlkZXISIwofREFUQVNPVVJDRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEh4KGkRBVEFTT1VSQ0VfUFJPVklERVJfR0lUSFVCEAESGwoXREFUQVNPVVJDRV9QUk9WSURFUl9BUEkQAhIfChtEQVRBU09VUkNFX1BST1ZJREVSX0NSQVdMRVIQAxIeChpEQVRBU09VUkNFX1BST1ZJREVSX1VQTE9BRBAEKpEBChBEYXRhc291cmNlU3RhdHVzEiEKHURBVEFTT1VSQ0VfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHAoYREFUQVNPVVJDRV9TVEFUVVNfQUNUSVZFEAESHAoYREFUQVNPVVJDRV9TVEFUVVNfUEFVU0VEEAISHgoaREFUQVNPVVJDRV9TVEFUVVNfREVHUkFERUQQAyq8AQoWRGF0YXNvdXJjZUxpdmVEZWxpdmVyeRIoCiREQVRBU09VUkNFX0xJVkVfREVMSVZFUllfVU5TUEVDSUZJRUQQABIhCh1EQVRBU09VUkNFX0xJVkVfREVMSVZFUllfTk9ORRABEisKJ0RBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9TT1VSQ0VfV0VCSE9PSxACEigKJERBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9BUFBfV0VCSE9PSxADKtYBChFBcGlDcmVkZW50aWFsS2luZBIjCh9BUElfQ1JFREVOVElBTF9LSU5EX1VOU1BFQ0lGSUVEEAASHgoaQVBJX0NSRURFTlRJQUxfS0lORF9CRUFSRVIQARIdChlBUElfQ1JFREVOVElBTF9LSU5EX0JBU0lDEAISHgoaQVBJX0NSRURFTlRJQUxfS0lORF9IRUFERVIQAxIdChlBUElfQ1JFREVOVElBTF9LSU5EX1FVRVJZEAQSHgoaQVBJX0NSRURFTlRJQUxfS0lORF9PQVVUSDIQBSqmAgocRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRIuCipEQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfVU5TUEVDSUZJRUQQABIoCiREQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfRklMRVMQARIoCiREQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfUEFHRVMQAhIqCiZEQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfUkVDT1JEUxADEisKJ0RBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9NRVNTQUdFUxAEEikKJURBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9FVkVOVFMQBSrsAQoYRGF0YXNvdXJjZUNyZWRlbnRpYWxNb2RlEioKJkRBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX1VOU1BFQ0lGSUVEEAASIwofREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfTk9ORRABEiYKIkRBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX09SR19BUFAQAhIpCiVEQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9VU0VSX09BVVRIEAMSLAooREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfU1RBVElDX1NFQ1JFVBAEKpcBChZEYXRhc291cmNlUmVhZGVyc01vZGVsEigKJERBVEFTT1VSQ0VfUkVBREVSU19NT0RFTF9VTlNQRUNJRklFRBAAEioKJkRBVEFTT1VSQ0VfUkVBREVSU19NT0RFTF9TT1VSQ0VfU0NPUEVEEAESJwojREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1RSQU5TTEFURUQQAiruAQoPU291cmNlU3luY1BoYXNlEiEKHVNPVVJDRV9TWU5DX1BIQVNFX1VOU1BFQ0lGSUVEEAASHAoYU09VUkNFX1NZTkNfUEhBU0VfUVVFVUVEEAESHgoaU09VUkNFX1NZTkNfUEhBU0VfRkVUQ0hJTkcQAhIeChpTT1VSQ0VfU1lOQ19QSEFTRV9DT01QSUxFRBADEiAKHFNPVVJDRV9TWU5DX1BIQVNFX0hBTkRFRF9PRkYQBBIaChZTT1VSQ0VfU1lOQ19QSEFTRV9ET05FEAUSHAoYU09VUkNFX1NZTkNfUEhBU0VfRkFJTEVEEAYqpgMKF1NvdXJjZVN5bmNGYWlsdXJlUmVhc29uEioKJlNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX1VOU1BFQ0lGSUVEEAASKwonU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fUkFURV9MSU1JVEVEEAESKQolU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fQ1JFREVOVElBTBACEiwKKFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0FDQ0VTU19ERU5JRUQQAxIoCiRTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9OT1RfRk9VTkQQBBIoCiRTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9UT09fTEFSR0UQBRIvCitTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9IT1NUX1VOQVZBSUxBQkxFEAYSJAogU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fT1RIRVIQBxIuCipTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9ERUxJVkVSWV9GQUlMRUQQCCqcAQoRU291cmNlU3luY1RyaWdnZXISIwofU09VUkNFX1NZTkNfVFJJR0dFUl9VTlNQRUNJRklFRBAAEh4KGlNPVVJDRV9TWU5DX1RSSUdHRVJfTUFOVUFMEAESIQodU09VUkNFX1NZTkNfVFJJR0dFUl9TQ0hFRFVMRUQQAhIfChtTT1VSQ0VfU1lOQ19UUklHR0VSX1dFQkhPT0sQAyqPAQoWRGF0YXNvdXJjZURvbWFpblN0YXR1cxIoCiREQVRBU09VUkNFX0RPTUFJTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIkCiBEQVRBU09VUkNFX0RPTUFJTl9TVEFUVVNfUEVORElORxABEiUKIURBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19WRVJJRklFRBACKpUDChpTb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhIsCihTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1VOU1BFQ0lGSUVEEAASKAokU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9SRVZPS0VEEAESKQolU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9SRVBMQUNFRBACEi8KK1NPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fU09VUkNFX0RFTEVURUQQAxIvCitTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX01FTUJFUl9SRU1PVkVEEAQSMAosU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9QRVJNSVNTSU9OX0xPU1QQBRIuCipTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1VTRVJfSU5BQ1RJVkUQBhIwCixTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX0JJTkRJTkdfQ0hBTkdFRBAHMok4ChFEYXRhc291cmNlU2VydmljZRK5AwocUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0cxI1LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLlBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNSZXNwb25zZSKpAsLzGGwIAhADGhFkYXRhc291cmNlOmludm9rZSIRZGF0YXNvdXJjZTppbnZva2UqDwoJc291cmNlX2lkEAQYAzABOiMKH3NhYXMuZGF0YXNvdXJjZS5yZWNlaXB0cy5wcnVuZWQQAkADSARQA1gDYAHa8xi0AQoCCDwSAwjYBBgFIgIIAioLVU5BVkFJTEFCTEUqElJFU09VUkNFX0VYSEFVU1RFRCoHQUJPUlRFRDIPc2Fhcy1kYXRhc291cmNlSkYvc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlU2VydmljZS9Mb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUIAgWIAgYAFyGAoGc291cmNlEgZpbnZva2USBHJlYWQYARLmAQoiTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0cxI7LnNhYXMuYWNjb3VudHMudjEuTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLlBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNSZXNwb25zZSJLwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUANYA2ABEvwDChVJbnZva2VTb3VyY2VPcGVyYXRpb24SLi5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlIoEDwvMYbggCEAMaEWRhdGFzb3VyY2U6aW52b2tlIhFkYXRhc291cmNlOmludm9rZSoPCglzb3VyY2VfaWQQBBgDMAE6JQohc2Fhcy5kYXRhc291cmNlLm9wZXJhdGlvbi5pbnZva2VkEARAA0gEUARYBGAB2vMYrwEKAgg8EgMI2AQYBSICCAIqC1VOQVZBSUxBQkxFKhJSRVNPVVJDRV9FWEhBVVNURUQqB0FCT1JURUQyD3NhYXMtZGF0YXNvdXJjZUo/L3NhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVNlcnZpY2UvTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uUICABFiAgEBgAXIYCgZzb3VyY2USBmludm9rZRIEcmVhZBgBgtPkkwJVOgEqIlAvdjEvb3JnYW5pemF0aW9ucy97b3JnX2lkfS9kYXRhc291cmNlcy97c291cmNlX2lkfS9vcGVyYXRpb25zL3tvcGVyYXRpb259Omludm9rZRKHAgobTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uEjQuc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5JbnZva2VTb3VyY2VPcGVyYXRpb25SZXNwb25zZSKAAcLzGEcIAhADGg9kYXRhc291cmNlOnJlYWQiD2RhdGFzb3VyY2U6cmVhZCoPCgllZmZlY3RfaWQQBBgDMAE6AhABQAFIA1ADWARgAYLT5JMCLxItL3YxL2RhdGFzb3VyY2Utb3BlcmF0aW9uLXJlY2VpcHRzL3tlZmZlY3RfaWR9EoUCChRMaXN0U291cmNlT3BlcmF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdFNvdXJjZU9wZXJhdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlIo0BwvMYRAgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwI/Ej0vdjEvb3JnYW5pemF0aW9ucy97b3JnX2lkfS9kYXRhc291cmNlcy97c291cmNlX2lkfS9vcGVyYXRpb25zEpMCChdEZWNsYXJlU291cmNlT3BlcmF0aW9ucxIwLnNhYXMuYWNjb3VudHMudjEuRGVjbGFyZVNvdXJjZU9wZXJhdGlvbnNSZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlIpIBwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5vcGVyYXRpb24uZGVjbGFyZWQQAkABSARQA1gDYAGC0+STAkI6ASoiPS92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMSrAEKD0FkZEdpdEh1YlNvdXJjZRIoLnNhYXMuYWNjb3VudHMudjEuQWRkR2l0SHViU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuQWRkR2l0SHViU291cmNlUmVzcG9uc2UiRMLzGEAIAhAEKgwKBm9yZ19pZBACGAEwATogChxzYWFzLmRhdGFzb3VyY2Uuc291cmNlLmFkZGVkEAJAAUgEUARYA2ABEpoBCglBZGRTb3VyY2USIi5zYWFzLmFjY291bnRzLnYxLkFkZFNvdXJjZVJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLkFkZFNvdXJjZVJlc3BvbnNlIkTC8xhACAIQBCoMCgZvcmdfaWQQAhgBMAE6IAocc2Fhcy5kYXRhc291cmNlLnNvdXJjZS5hZGRlZBACQAFIBFAEWANgARKPAQoUR2V0RGF0YXNvdXJjZUNhdGFsb2cSLS5zYWFzLmFjY291bnRzLnYxLkdldERhdGFzb3VyY2VDYXRhbG9nUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgDUAJYAmABEoIBCgtMaXN0U291cmNlcxIkLnNhYXMuYWNjb3VudHMudjEuTGlzdFNvdXJjZXNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlc1Jlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARJ8CglHZXRTb3VyY2USIi5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVJlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKeAQoKU3luY1NvdXJjZRIjLnNhYXMuYWNjb3VudHMudjEuU3luY1NvdXJjZVJlcXVlc3QaJC5zYWFzLmFjY291bnRzLnYxLlN5bmNTb3VyY2VSZXNwb25zZSJFwvMYQQgCEAQqDAoGb3JnX2lkEAIYATABOiEKHXNhYXMuZGF0YXNvdXJjZS5zb3VyY2Uuc3luY2VkEAJAAUgEUARYAmABEogBCg1HZXRTb3VyY2VTeW5jEiYuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VTeW5jUmVxdWVzdBonLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlU3luY1Jlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKlAQoMRGVsZXRlU291cmNlEiUuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVTb3VyY2VSZXF1ZXN0GiYuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVTb3VyY2VSZXNwb25zZSJGwvMYQggCEAQqDAoGb3JnX2lkEAIYATABOiIKHnNhYXMuZGF0YXNvdXJjZS5zb3VyY2UucmVtb3ZlZBACQAFIBFACWAJgARLEAQoTQmVnaW5HaXRIdWJBcHBTZXR1cBIsLnNhYXMuYWNjb3VudHMudjEuQmVnaW5HaXRIdWJBcHBTZXR1cFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkJlZ2luR2l0SHViQXBwU2V0dXBSZXNwb25zZSJQwvMYTAgCEAQqDAoGb3JnX2lkEAIYATABOiwKKHNhYXMuZGF0YXNvdXJjZS5naXRodWJfYXBwLnNldHVwX3N0YXJ0ZWQQAkABSARQAlgEYAESzwEKFkNvbXBsZXRlR2l0SHViQXBwU2V0dXASLy5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVzcG9uc2UiUsLzGE4IAhAEKgwKBm9yZ19pZBACGAEwATouCipzYWFzLmRhdGFzb3VyY2UuZ2l0aHViX2FwcC5zZXR1cF9jb21wbGV0ZWQQAkABSARQBFgDYAESzQEKGE1pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcBIxLnNhYXMuYWNjb3VudHMudjEuTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVxdWVzdBoyLnNhYXMuYWNjb3VudHMudjEuTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2UiSsLzGEYIAhAEKgwKBm9yZ19pZBACGAEwATomCiJzYWFzLmRhdGFzb3VyY2UuY3JlZGVudGlhbC51cGRhdGVkEAJAAUgEUAJYA2ABEtUBChpCZWdpbkRhdGFzb3VyY2VBY2NvdW50TGluaxIzLnNhYXMuYWNjb3VudHMudjEuQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0GjQuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkRhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkzC8xhICAIQAyoMCgZvcmdfaWQQAhgBMAE6KAokc2Fhcy5kYXRhc291cmNlLmFjY291bnRfbGlua19zdGFydGVkEAJAAUgEUAJYBGABEtgBCh1Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGluaxI2LnNhYXMuYWNjb3VudHMudjEuQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkbC8xhCCAIQAyoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmFjY291bnRfbGlua2VkEAJAAUgEUARYA2ABErUBChxMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzEjUuc2Fhcy5hY2NvdW50cy52MS5MaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1Jlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARLUAQobRGVsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rEjQuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZSJIwvMYRAgCEAMqDAoGb3JnX2lkEAIYATABOiQKIHNhYXMuZGF0YXNvdXJjZS5hY2NvdW50X3VubGlua2VkEAJAAUgEUAJYA2ABEqMBChZHZXREYXRhc291cmNlRGlyZWN0b3J5Ei8uc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlRGlyZWN0b3J5UmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZURpcmVjdG9yeVJlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARK3AQoTQmluZERhdGFzb3VyY2VHcm91cBIsLnNhYXMuYWNjb3VudHMudjEuQmluZERhdGFzb3VyY2VHcm91cFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJDwvMYPwgCEAQqDAoGb3JnX2lkEAIYATABOh8KG3NhYXMuZGF0YXNvdXJjZS5ncm91cF9ib3VuZBACQAFIBFACWANgARK/AQoVVW5iaW5kRGF0YXNvdXJjZUdyb3VwEi4uc2Fhcy5hY2NvdW50cy52MS5VbmJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJFwvMYQQgCEAQqDAoGb3JnX2lkEAIYATABOiEKHXNhYXMuZGF0YXNvdXJjZS5ncm91cF91bmJvdW5kEAJAAUgEUAJYA2ABEsABChVDbGFpbURhdGFzb3VyY2VEb21haW4SLi5zYWFzLmFjY291bnRzLnYxLkNsYWltRGF0YXNvdXJjZURvbWFpblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmRvbWFpbl9jbGFpbWVkEAJAAUgEUAJYA2ABEsQBChZWZXJpZnlEYXRhc291cmNlRG9tYWluEi8uc2Fhcy5hY2NvdW50cy52MS5WZXJpZnlEYXRhc291cmNlRG9tYWluUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuVmVyaWZ5RGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkfC8xhDCAIQBCoMCgZvcmdfaWQQAhgBMAE6Iwofc2Fhcy5kYXRhc291cmNlLmRvbWFpbl92ZXJpZmllZBACQAFIBFACWANgARLDAQoWRGVsZXRlRGF0YXNvdXJjZURvbWFpbhIvLnNhYXMuYWNjb3VudHMudjEuRGVsZXRlRGF0YXNvdXJjZURvbWFpblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VEb21haW5SZXNwb25zZSJGwvMYQggCEAQqDAoGb3JnX2lkEAIYATABOiIKHnNhYXMuZGF0YXNvdXJjZS5kb21haW5fcmVtb3ZlZBACQAFIBFACWANgARKgAQoVTGlzdFNvdXJjZURlbGVnYXRpb25zEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlRGVsZWdhdGlvbnNSZXNwb25zZSImwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQAlgDYAESxwEKFlJldm9rZVNvdXJjZURlbGVnYXRpb24SLy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTb3VyY2VEZWxlZ2F0aW9uUmVzcG9uc2UiSsLzGEYIAhAEKgwKBm9yZ19pZBACGAEwATomCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5yZXZva2VkEAJAAUgEUAJYA2ABEusBChZEZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzEi8uc2Fhcy5hY2NvdW50cy52MS5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuRGVsZWdhdGVTb3VyY2VUcmlnZ2Vyc1Jlc3BvbnNlIm7C8xhqCAIQBCoMCgZvcmdfaWQQAhgBMAE6Sgoic2Fhcy5kYXRhc291cmNlLmRlbGVnYXRpb24uY3JlYXRlZAoic2Fhcy5kYXRhc291cmNlLmRlbGVnYXRpb24ucmV2b2tlZBACQAFIBFACWANgARKyAQobR2V0U291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEjQuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXNwb25zZSImwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQAlgDYAESgwMKD1JlY29uY2lsZVNvdXJjZRIoLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVzcG9uc2UimgLC8xhqCAIQAxoRZGF0YXNvdXJjZTppbnZva2UiEWRhdGFzb3VyY2U6aW52b2tlKg8KCXNvdXJjZV9pZBAEGAMwATohCh1zYWFzLmRhdGFzb3VyY2Uuc291cmNlLnN5bmNlZBACQANIBFACWAJgAdrzGKcBCgIIPBIDCNgEGAUiAggCKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMg9zYWFzLWRhdGFzb3VyY2VKOS9zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTZXJ2aWNlL0xvb2t1cFJlY29uY2lsZVNvdXJjZVCAIFiAIGABchgKBnNvdXJjZRIGaW52b2tlEgRyZWFkGAESvwEKFUxvb2t1cFJlY29uY2lsZVNvdXJjZRIuLnNhYXMuYWNjb3VudHMudjEuTG9va3VwUmVjb25jaWxlU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVzcG9uc2UiS8LzGEcIAhADGg9kYXRhc291cmNlOnJlYWQiD2RhdGFzb3VyY2U6cmVhZCoPCgllZmZlY3RfaWQQBBgDMAE6AhABQAFIA1ACWAJgAWIGcHJvdG8z", [file_buf_validate_validate, file_codefly_runnable_v0_options, file_google_api_annotations, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * GitHubDatasourceConfig is the public, non-secret configuration of a GitHub @@ -2416,15 +2416,23 @@ export const RevokeSourceDelegationResponseSchema: GenMessage { // Which arm it names, said in words: a declaration that named neither // would have its occurrences refused, and the page says so rather than // leaving the field blank. - // It does not claim WHICH module's reference — the reference value is the - // source id and the references table answers that — but it does say the - // delegation is one the executing module can use, because the host sets - // the arm only for a delegation whose binding names its own module as - // the audience (issue #1121, m5). + // It names the HOLDER, which the arm's own value cannot carry, and + // refuses to claim that the holder is the module which executes this + // release — nothing the host holds declares that, so a source whose + // only self-addressed delegation belongs to another module would read + // as ready while the composed executor's mint was refused + // (issue #1121, F1). expect( - screen.getByText("A delegation reference the executing module holds"), + screen.getByText(/A delegation reference held by docstore/), ).toBeTruthy(); + expect(screen.getByText(/unverified/)).toBeTruthy(); // The reference an operator bound, with whose authority it carries and // exactly what it allows — never the module's own authority. diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/datasource_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/datasource_pb.ts index a667d68cb..e4f93e5df 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/datasource_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/datasource_pb.ts @@ -18,7 +18,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/datasource.proto. */ export const file_saas_accounts_v1_datasource: GenFile = /*@__PURE__*/ - fileDesc("CiFzYWFzL2FjY291bnRzL3YxL2RhdGFzb3VyY2UucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoWR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIMCgRyZXBvGAEgASgJEg0KBXBhdGhzGAIgAygJEg4KBmJyYW5jaBgDIAEoCRIXCg9maWxlX2V4dGVuc2lvbnMYBCADKAki9AEKD0FwaU9BdXRoMkNvbmZpZxI2CgVncmFudBgEIAEoDjInLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnLkdyYW50EhEKCXRva2VuX3VybBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSDgoGc2NvcGVzGAMgAygJInMKBUdyYW50EhUKEUdSQU5UX1VOU1BFQ0lGSUVEEAASFwoTR1JBTlRfUkVGUkVTSF9UT0tFThABEhwKGEdSQU5UX0NMSUVOVF9DUkVERU5USUFMUxACEhwKGEdSQU5UX0FVVEhPUklaQVRJT05fQ09ERRADIuoBChNBcGlEYXRhc291cmNlQ29uZmlnEhAKCGJhc2VfdXJsGAEgASgJEhUKDXJlc291cmNlX3BhdGgYAiABKAkSPAoPY3JlZGVudGlhbF9raW5kGAMgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5BcGlDcmVkZW50aWFsS2luZBIZChFjcmVkZW50aWFsX2hlYWRlchgEIAEoCRIeChZjcmVkZW50aWFsX3F1ZXJ5X3BhcmFtGAUgASgJEjEKBm9hdXRoMhgGIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnIkEKF0NyYXdsZXJEYXRhc291cmNlQ29uZmlnEhMKC3NpdGVtYXBfdXJsGAEgASgJEhEKCW1heF9wYWdlcxgCIAEoDSKGAQoWVXBsb2FkRGF0YXNvdXJjZUNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIOCgZyZWdpb24YAiABKAkSDgoGYnVja2V0GAMgASgJEg4KBnByZWZpeBgEIAEoCRIVCg1hY2Nlc3Nfa2V5X2lkGAUgASgJEhMKC21heF9vYmplY3RzGAYgASgNIoMHCgpEYXRhc291cmNlEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRI2Cghwcm92aWRlchgDIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEjgKBmdpdGh1YhgFIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIyCgZzdGF0dXMYBiABKA4yIi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTdGF0dXMSGgoSd2ViaG9va19jb25maWd1cmVkGAcgASgIEi4KCmNyZWF0ZWRfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDmxhc3Rfc3luY2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCgNhcGkYCyABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWcSOgoHY3Jhd2xlchgMIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuQ3Jhd2xlckRhdGFzb3VyY2VDb25maWcSOAoGdXBsb2FkGA0gASgLMiguc2Fhcy5hY2NvdW50cy52MS5VcGxvYWREYXRhc291cmNlQ29uZmlnEhgKEGJvdW5kYXJ5X25vZGVfaWQYDiABKAkSNAoQbGFzdF9pbmdlc3RlZF9hdBgPIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoUbGFzdF9pbmdlc3RlZF9jb21taXQYECABKAkSFQoNc3RhdHVzX3JlYXNvbhgRIAEoCRIWCg5ib3VuZGFyeV9sYWJlbBgSIAEoCRISCgpjb25mb3JtYW50GBMgASgIEhcKD2NvbmZvcm1hbmNlX2dhcBgUIAEoCRI/Cg1saXZlX2RlbGl2ZXJ5GBUgASgOMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlTGl2ZURlbGl2ZXJ5EjUKEnJlY29uY2lsZV9pbnRlcnZhbBgWIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkoECAQQBVIRdGFyZ2V0X2NvbGxlY3Rpb24isgMKFkFkZEdpdEh1YlNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI7CgRyZXBvGAIgASgJQi26SCpyKBADGP8BMiFeW0EtWmEtejAtOV8uLV0rL1tBLVphLXowLTlfLi1dKyQSIAoFcGF0aHMYAyADKAlCEbpIDpIBCxBAIgdyBRABGIAEEhgKBmJyYW5jaBgEIAEoCUIIukgFcgMY/wESJAoQYm91bmRhcnlfbm9kZV9pZBgIIAEoCUIIukgFcgOwAQFIABImChBjb2xsZWN0aW9uX2xhYmVsGAkgASgJQgq6SAdyBRABGP8BSAASHgoMYWNjZXNzX3Rva2VuGAYgASgJQgi6SAVyAxiACBIgCg53ZWJob29rX3NlY3JldBgHIAEoCUIIukgFcgMYgAgSSQoPZmlsZV9leHRlbnNpb25zGAogAygJQjC6SC2SASoQICImciQQAhghMh5eXC5bQS1aYS16MC05XVtBLVphLXowLTkuXy1dKiRCEQoIYm91bmRhcnkSBbpIAggBSgQIBRAGUhF0YXJnZXRfY29sbGVjdGlvbiJLChdBZGRHaXRIdWJTb3VyY2VSZXNwb25zZRIwCgpkYXRhc291cmNlGAEgASgLMhwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlIsgEChBBZGRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESQgoIcHJvdmlkZXIYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckIKukgHggEEEAEgABI6CgZnaXRodWIYBCABKAsyKC5zYWFzLmFjY291bnRzLnYxLkdpdEh1YkRhdGFzb3VyY2VDb25maWdIABI0CgNhcGkYBSABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWdIABI8CgdjcmF3bGVyGAggASgLMikuc2Fhcy5hY2NvdW50cy52MS5DcmF3bGVyRGF0YXNvdXJjZUNvbmZpZ0gAEjoKBnVwbG9hZBgJIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuVXBsb2FkRGF0YXNvdXJjZUNvbmZpZ0gAEiQKEGJvdW5kYXJ5X25vZGVfaWQYDCABKAlCCLpIBXIDsAEBSAESJgoQY29sbGVjdGlvbl9sYWJlbBgNIAEoCUIKukgHcgUQARj/AUgBEhwKCmNyZWRlbnRpYWwYBiABKAlCCLpIBXIDGIAgEiAKDndlYmhvb2tfc2VjcmV0GAcgASgJQgi6SAVyAxiACBImChRvYXV0aDJfY2xpZW50X3NlY3JldBgKIAEoCUIIukgFcgMYgAhCCAoGY29uZmlnQhEKCGJvdW5kYXJ5EgW6SAIIAUoECAMQBFIRdGFyZ2V0X2NvbGxlY3Rpb24iRQoRQWRkU291cmNlUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJaChVEYXRhc291cmNlQ29uZmlnRmllbGQSCwoDa2V5GAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIMCgRoZWxwGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIsQBChlEYXRhc291cmNlQ29ubmVjdG9yQnVkZ2V0EhoKEm1heF9pdGVtc19wZXJfY2FsbBgBIAEoDRIaChJtYXhfYnl0ZXNfcGVyX2NhbGwYAiABKAMSFgoObWF4X2l0ZW1fYnl0ZXMYAyABKAMSHQoVb3BlcmF0aW9uc19wZXJfd2luZG93GAQgASgNEhYKDndpbmRvd19zZWNvbmRzGAUgASgDEiAKGGJhY2tncm91bmRfc2hhcmVfcGVyY2VudBgGIAEoDSKqBgocRGF0YXNvdXJjZVByb3ZpZGVyRGVzY3JpcHRvchI2Cghwcm92aWRlchgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRI+Cg1jb25maWdfZmllbGRzGAQgAygLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ29uZmlnRmllbGQSGAoQc3VwcG9ydHNfd2ViaG9vaxgFIAEoCBJHChpzdXBwb3J0ZWRfY3JlZGVudGlhbF9raW5kcxgGIAMoDjIjLnNhYXMuYWNjb3VudHMudjEuQXBpQ3JlZGVudGlhbEtpbmQSEQoJY29ubmVjdG9yGAcgASgJEkEKCWludGVyZmFjZRgIIAEoDjIuLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRJEChBjcmVkZW50aWFsX21vZGVzGAkgAygOMiouc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSPwoNcmVhZGVyc19tb2RlbBgKIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVJlYWRlcnNNb2RlbBI7CgZidWRnZXQYCyABKAsyKy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VDb25uZWN0b3JCdWRnZXQSEgoKY29uZm9ybWFudBgMIAEoCBIXCg9jb25mb3JtYW5jZV9nYXAYDSABKAkSGwoTYWNjZXB0c19uZXdfc291cmNlcxgOIAEoCBIgChhsaXZlX2RlbGl2ZXJ5X2NvbmZpZ3VyZWQYDyABKAgSGgoSYWNjZXB0c19vcGVyYXRpb25zGBAgASgIEhYKDm9wZXJhdGlvbnNfZ2FwGBEgASgJEhsKE29hdXRoX2F1dGhvcml6ZV91cmwYEiABKAkSFwoPb2F1dGhfdG9rZW5fdXJsGBMgASgJEhQKDG9hdXRoX3Njb3BlcxgUIAMoCSIdChtHZXREYXRhc291cmNlQ2F0YWxvZ1JlcXVlc3QiYQocR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZRJBCglwcm92aWRlcnMYASADKAsyLi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckRlc2NyaXB0b3IiLgoSTGlzdFNvdXJjZXNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiSAoTTGlzdFNvdXJjZXNSZXNwb25zZRIxCgtkYXRhc291cmNlcxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJCChBHZXRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIkUKEUdldFNvdXJjZVJlc3BvbnNlEjAKCmRhdGFzb3VyY2UYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2UidgoRU3luY1NvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQESHgoMYWNjZXNzX3Rva2VuGAMgASgJQgi6SAVyAxiAIBIRCglyZWNvbm5lY3QYBCABKAgiJAoSU3luY1NvdXJjZVJlc3BvbnNlEg4KBmpvYl9pZBgBIAEoCSJqChRHZXRTb3VyY2VTeW5jUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoGam9iX2lkGAMgASgJQgu6SAjYAQFyA7ABASKVAQoSU291cmNlU3luY0RlbGl2ZXJ5EhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESLQoFc3RhdGUYAiABKA4yFi5zYWFzLmpvYnMudjEuSm9iU3RhdGVCBrpIA8gBARI2CglleGVjdXRpb24YAyABKAsyIy5zYWFzLmpvYnMudjEuSm9iRXhlY3V0aW9uUmVmZXJlbmNlIosBChFTb3VyY2VTeW5jQ2hhbmdlcxINCgVmaWxlcxgBIAEoDRINCgVhZGRlZBgCIAEoDRIQCghtb2RpZmllZBgDIAEoDRIPCgdkZWxldGVkGAQgASgNEhMKC3NwbGl0X2tub3duGAUgASgIEhAKCHNuYXBzaG90GAYgASgIEg4KBmNvbW1pdBgHIAEoCSKtAQoRU291cmNlU3luY0ZhaWx1cmUSOQoGcmVhc29uGAEgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jRmFpbHVyZVJlYXNvbhIMCgRjb2RlGAIgASgJEg8KB21lc3NhZ2UYAyABKAkSEAoIcmV0cnlpbmcYBCABKAgSLAoIcmV0cnlfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoQEChJTb3VyY2VTeW5jUHJvZ3Jlc3MSMAoFcGhhc2UYASABKA4yIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNQaGFzZRI0Cgd0cmlnZ2VyGAIgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jVHJpZ2dlchItCglxdWV1ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2ZldGNoaW5nX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtjb21waWxlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMQoNaGFuZGVkX29mZl9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZmluaXNoZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjQKB2NoYW5nZXMYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNDaGFuZ2VzEjQKB2ZhaWx1cmUYCSABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNGYWlsdXJlEg8KB2F0dGVtcHQYCiABKA0SFAoMbWF4X2F0dGVtcHRzGAsgASgNItIBChVHZXRTb3VyY2VTeW5jUmVzcG9uc2USGAoGam9iX2lkGAEgASgJQgi6SAVyA7ABARItCgVzdGF0ZRgCIAEoDjIWLnNhYXMuam9icy52MS5Kb2JTdGF0ZUIGukgDyAEBEjgKCmRlbGl2ZXJpZXMYAyADKAsyJC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNEZWxpdmVyeRI2Cghwcm9ncmVzcxgEIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1Byb2dyZXNzIkUKE0RlbGV0ZVNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiFgoURGVsZXRlU291cmNlUmVzcG9uc2UiVgoTR2l0SHViQXBwUmVwb3NpdG9yeRIMCgRyZXBvGAEgASgJEhYKDmRlZmF1bHRfYnJhbmNoGAIgASgJEhkKEWFscmVhZHlfY29ubmVjdGVkGAMgASgIIjYKGkJlZ2luR2l0SHViQXBwU2V0dXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEicQobQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlEhMKC2luc3RhbGxfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIpwBCh1Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGP8BEiwKD2luc3RhbGxhdGlvbl9pZBgDIAEoCUITukgQcg4QARggMgheWzAtOV0rJBIYCgRjb2RlGAQgASgJQgq6SAdyBRABGP8BInYKHkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSOwoMcmVwb3NpdG9yaWVzGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5HaXRIdWJBcHBSZXBvc2l0b3J5IlEKH01pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiVAogTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSLEAQoVRGF0YXNvdXJjZUFjY291bnRMaW5rEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRIPCgd1c2VyX2lkGAMgASgJEhEKCWNvbm5lY3RvchgEIAEoCRIbChNwcm92aWRlcl9hY2NvdW50X2lkGAUgASgJEh4KFnByb3ZpZGVyX2FjY291bnRfbG9naW4YBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAitwEKFkRhdGFzb3VyY2VHcm91cEJpbmRpbmcSCgoCaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhEKCWNvbm5lY3RvchgDIAEoCRIZChFwcm92aWRlcl9ncm91cF9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJEhIKCmNyZWF0ZWRfYnkYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilAIKGERhdGFzb3VyY2VWZXJpZmllZERvbWFpbhIKCgJpZBgBIAEoCRIOCgZvcmdfaWQYAiABKAkSDgoGZG9tYWluGAMgASgJEjgKBnN0YXR1cxgEIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURvbWFpblN0YXR1cxIXCg90eHRfcmVjb3JkX25hbWUYBSABKAkSGAoQdHh0X3JlY29yZF92YWx1ZRgGIAEoCRIvCgt2ZXJpZmllZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKY3JlYXRlZF9hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiMwoXRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCSKmAQohQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSHgoMcmVkaXJlY3RfdXJpGAMgASgJQgi6SAVyAxiAEBIRCglzb3VyY2VfaWQYBCABKAkiegoiQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRIVCg1hdXRob3JpemVfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInUKJENvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGIBAEhgKBGNvZGUYAyABKAlCCrpIB3IFEAEY/wEiXgolQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRI1CgRsaW5rGAEgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQWNjb3VudExpbmsiPwojTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJeCiRMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2USNgoFbGlua3MYASADKAsyJy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VBY2NvdW50TGluayJUCiJEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIiUKI0RlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIjkKHUdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiiwIKHkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXNwb25zZRI2CgVsaW5rcxgBIAMoCzInLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUFjY291bnRMaW5rEjoKCGJpbmRpbmdzGAIgAygLMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlR3JvdXBCaW5kaW5nEjsKB2RvbWFpbnMYAyADKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbhI4CgV0ZWFtcxgEIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0irgEKGkJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSJQoRcHJvdmlkZXJfZ3JvdXBfaWQYAyABKAlCCrpIB3IFEAEY/wESGQoHdGVhbV9pZBgEIAEoCUIIukgFcgOwAQEiWAobQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlEjkKB2JpbmRpbmcYASABKAsyKC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VHcm91cEJpbmRpbmciTgocVW5iaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIfCh1VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJUChxDbGFpbURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGgoGZG9tYWluGAIgASgJQgq6SAdyBRADGP0BIlsKHUNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlEjoKBmRvbWFpbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVZlcmlmaWVkRG9tYWluIk8KHVZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlwKHlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXNwb25zZRI6CgZkb21haW4YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbiJPCh1EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIgCh5EZWxldGVEYXRhc291cmNlRG9tYWluUmVzcG9uc2UingIKEFNvdXJjZURlbGVnYXRpb24SCgoCaWQYASABKAkSEQoJc291cmNlX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIOCgZtb2R1bGUYBCABKAkSDwoHYmluZGluZxgFIAEoCRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpyZXZva2VkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBJACgpyZXZvY2F0aW9uGAggASgOMiwuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhISCgpyZXZva2VkX2J5GAkgASgJIoEBChxMaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIAoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABAUgAiAEBEhcKD2luY2x1ZGVfcmV2b2tlZBgDIAEoCEIMCgpfc291cmNlX2lkIlgKHUxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlEjcKC2RlbGVnYXRpb25zGAEgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIk8KHVJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlgKHlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZRI2CgpkZWxlZ2F0aW9uGAEgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIjIKD1RyaWdnZXJJZGVudGl0eRIOCgZzb3VyY2UYASABKAkSDwoHYmluZGluZxgCIAEoCSKEAQoXRGVjbGFyZWRUcmlnZ2VyU2NoZWR1bGUSDAoEY3JvbhgBIAEoCRIQCgh0aW1lem9uZRgCIAEoCRISCgptaXNzZWRfcnVuGAMgASgJEg8KB292ZXJsYXAYBCABKAkSEAoIZGVhZGxpbmUYBSABKAMSEgoKaW5wdXRfanNvbhgGIAEoCSLnAgoaVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USEQoJcmVmZXJlbmNlGAEgASgJEhUKDWRlbGVnYXRpb25faWQYAiABKAkSDgoGbW9kdWxlGAMgASgJEg8KB2JpbmRpbmcYBCABKAkSEAoIYXVkaWVuY2UYBSABKAkSDgoGdGVuYW50GAYgASgJEhoKEm93bmVyX3ByaW5jaXBhbF9pZBgHIAEoCRIOCgZzY29wZXMYCCADKAkSDgoGYWN0aXZlGAkgASgIEi4KCmNyZWF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnJldm9rZWRfYXQYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEkAKCnJldm9jYXRpb24YDCABKA4yLC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uIr4BChNUcmlnZ2VyQ2hpbGRDb250ZXh0EhUKDWRlbGVnYXRpb25faWQYASABKAkSDgoGbW9kdWxlGAIgASgJEg8KB2JpbmRpbmcYAyABKAkSEAoIYXVkaWVuY2UYBCABKAkSDgoGc2NvcGVzGAUgAygJEg4KBmxvb2t1cBgGIAEoCBItCgltaW50ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBnJlY29yZBgIIAEoCSKgAwoYU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEg8KB3JlbGVhc2UYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEjsKCHNjaGVkdWxlGAMgASgLMikuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlZFRyaWdnZXJTY2hlZHVsZRIzCghpZGVudGl0eRgEIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlcklkZW50aXR5Eg4KBmRpZ2VzdBgFIAEoCRI2ChJsYXN0X29jY3VycmVuY2VfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjYKEm5leHRfb2NjdXJyZW5jZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTdW5kZWNsYXJhYmxlX3JlYXNvbhgIIAEoCRIaChJob3N0X3N3ZWVwX2VuYWJsZWQYCSABKAgSNQoRaG9zdF9zd2VlcF9kdWVfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIlYKHURlbGVnYXRlU291cmNlVHJpZ2dlcnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABASJjCh5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVzcG9uc2USQQoLZGVsZWdhdGlvbnMYASADKAsyLC5zYWFzLmFjY291bnRzLnYxLlRyaWdnZXJEZWxlZ2F0aW9uUmVmZXJlbmNlIlsKIkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBIuIBCiNHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXNwb25zZRI/CgtkZWNsYXJhdGlvbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEkEKC2RlbGVnYXRpb25zGAIgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5UcmlnZ2VyRGVsZWdhdGlvblJlZmVyZW5jZRI3CghjaGlsZHJlbhgDIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlckNoaWxkQ29udGV4dCJuChZSZWNvbmNpbGVTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABARIdCgllZmZlY3RfaWQYAyABKAlCCrpIB3IFEAEYgAEiTAoXUmVjb25jaWxlU291cmNlUmVzcG9uc2USEQoJZWZmZWN0X2lkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIOCgZqb2JfaWQYAyABKAkiPQocTG9va3VwUmVjb25jaWxlU291cmNlUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEYgAEi8QIKD1NvdXJjZU9wZXJhdGlvbhIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg4KBm1ldGhvZBgDIAEoCRIMCgRwYXRoGAQgASgJEg0KBXF1ZXJ5GAUgAygJEi0KDGlucHV0X3NjaGVtYRgGIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSLgoNb3V0cHV0X3NjaGVtYRgHIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QSOAoGZWZmZWN0GAggASgOMiguc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VPcGVyYXRpb24uRWZmZWN0EhgKEG1heF9vdXRwdXRfYnl0ZXMYCSABKA0SDgoGZGlnZXN0GAogASgJIksKBkVmZmVjdBIWChJFRkZFQ1RfVU5TUEVDSUZJRUQQABIUChBFRkZFQ1RfUkVBRF9PTkxZEAESEwoPRUZGRUNUX01VVEFUSU9OEAIijgEKHkRlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESNQoKb3BlcmF0aW9ucxgDIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIlgKH0RlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2USNQoKb3BlcmF0aW9ucxgBIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIlQKG0xpc3RTb3VyY2VPcGVyYXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQEiVQocTGlzdFNvdXJjZU9wZXJhdGlvbnNSZXNwb25zZRI1CgpvcGVyYXRpb25zGAEgAygLMiEuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VPcGVyYXRpb24iwwEKHEludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEjAKCW9wZXJhdGlvbhgDIAEoCUIdukgachgyFl5bYS16XVthLXowLTlfXXswLDYzfSQSHQoKaW5wdXRfanNvbhgEIAEoCUIJukgGcgQogIAEEhsKCWVmZmVjdF9pZBgFIAEoCUIIukgFcgMogAEifwoWU291cmNlT3BlcmF0aW9uUmVjZWlwdBIRCgllZmZlY3RfaWQYASABKAkSFAoMY29tbWl0dGVkX2F0GAIgASgJEg4KBnN0YXR1cxgDIAEoCRIXCg9wcm92aWRlcl9zdGF0dXMYBCABKA0SEwoLb3V0cHV0X2pzb24YBSABKAkibwodSW52b2tlU291cmNlT3BlcmF0aW9uUmVzcG9uc2USEwoLb3V0cHV0X2pzb24YASABKAkSOQoHcmVjZWlwdBgCIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uUmVjZWlwdCJDCiJMb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0Eh0KCWVmZmVjdF9pZBgBIAEoCUIKukgHcgUQASiAASJ5CiNQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoJZWZmZWN0X2lkGAMgASgJQgi6SAVyAyiAASJbCiRQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVzcG9uc2USEQoJZWZmZWN0X2lkGAEgASgJEhAKCHJlY2VpcHRzGAIgASgDEg4KBnN0YXR1cxgDIAEoCSJKCilMb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEogAEqtwEKEkRhdGFzb3VyY2VQcm92aWRlchIjCh9EQVRBU09VUkNFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASHgoaREFUQVNPVVJDRV9QUk9WSURFUl9HSVRIVUIQARIbChdEQVRBU09VUkNFX1BST1ZJREVSX0FQSRACEh8KG0RBVEFTT1VSQ0VfUFJPVklERVJfQ1JBV0xFUhADEh4KGkRBVEFTT1VSQ0VfUFJPVklERVJfVVBMT0FEEAQqkQEKEERhdGFzb3VyY2VTdGF0dXMSIQodREFUQVNPVVJDRV9TVEFUVVNfVU5TUEVDSUZJRUQQABIcChhEQVRBU09VUkNFX1NUQVRVU19BQ1RJVkUQARIcChhEQVRBU09VUkNFX1NUQVRVU19QQVVTRUQQAhIeChpEQVRBU09VUkNFX1NUQVRVU19ERUdSQURFRBADKrwBChZEYXRhc291cmNlTGl2ZURlbGl2ZXJ5EigKJERBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEiEKHURBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9OT05FEAESKwonREFUQVNPVVJDRV9MSVZFX0RFTElWRVJZX1NPVVJDRV9XRUJIT09LEAISKAokREFUQVNPVVJDRV9MSVZFX0RFTElWRVJZX0FQUF9XRUJIT09LEAMq1gEKEUFwaUNyZWRlbnRpYWxLaW5kEiMKH0FQSV9DUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIeChpBUElfQ1JFREVOVElBTF9LSU5EX0JFQVJFUhABEh0KGUFQSV9DUkVERU5USUFMX0tJTkRfQkFTSUMQAhIeChpBUElfQ1JFREVOVElBTF9LSU5EX0hFQURFUhADEh0KGUFQSV9DUkVERU5USUFMX0tJTkRfUVVFUlkQBBIeChpBUElfQ1JFREVOVElBTF9LSU5EX09BVVRIMhAFKqYCChxEYXRhc291cmNlQ29ubmVjdG9ySW50ZXJmYWNlEi4KKkRBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9VTlNQRUNJRklFRBAAEigKJERBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9GSUxFUxABEigKJERBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9QQUdFUxACEioKJkRBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9SRUNPUkRTEAMSKwonREFUQVNPVVJDRV9DT05ORUNUT1JfSU5URVJGQUNFX01FU1NBR0VTEAQSKQolREFUQVNPVVJDRV9DT05ORUNUT1JfSU5URVJGQUNFX0VWRU5UUxAFKuwBChhEYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSKgomREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfVU5TUEVDSUZJRUQQABIjCh9EQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9OT05FEAESJgoiREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfT1JHX0FQUBACEikKJURBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX1VTRVJfT0FVVEgQAxIsCihEQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9TVEFUSUNfU0VDUkVUEAQqlwEKFkRhdGFzb3VyY2VSZWFkZXJzTW9kZWwSKAokREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1VOU1BFQ0lGSUVEEAASKgomREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1NPVVJDRV9TQ09QRUQQARInCiNEQVRBU09VUkNFX1JFQURFUlNfTU9ERUxfVFJBTlNMQVRFRBACKu4BCg9Tb3VyY2VTeW5jUGhhc2USIQodU09VUkNFX1NZTkNfUEhBU0VfVU5TUEVDSUZJRUQQABIcChhTT1VSQ0VfU1lOQ19QSEFTRV9RVUVVRUQQARIeChpTT1VSQ0VfU1lOQ19QSEFTRV9GRVRDSElORxACEh4KGlNPVVJDRV9TWU5DX1BIQVNFX0NPTVBJTEVEEAMSIAocU09VUkNFX1NZTkNfUEhBU0VfSEFOREVEX09GRhAEEhoKFlNPVVJDRV9TWU5DX1BIQVNFX0RPTkUQBRIcChhTT1VSQ0VfU1lOQ19QSEFTRV9GQUlMRUQQBiqmAwoXU291cmNlU3luY0ZhaWx1cmVSZWFzb24SKgomU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fVU5TUEVDSUZJRUQQABIrCidTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9SQVRFX0xJTUlURUQQARIpCiVTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9DUkVERU5USUFMEAISLAooU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fQUNDRVNTX0RFTklFRBADEigKJFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX05PVF9GT1VORBAEEigKJFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX1RPT19MQVJHRRAFEi8KK1NPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0hPU1RfVU5BVkFJTEFCTEUQBhIkCiBTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9PVEhFUhAHEi4KKlNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0RFTElWRVJZX0ZBSUxFRBAIKpwBChFTb3VyY2VTeW5jVHJpZ2dlchIjCh9TT1VSQ0VfU1lOQ19UUklHR0VSX1VOU1BFQ0lGSUVEEAASHgoaU09VUkNFX1NZTkNfVFJJR0dFUl9NQU5VQUwQARIhCh1TT1VSQ0VfU1lOQ19UUklHR0VSX1NDSEVEVUxFRBACEh8KG1NPVVJDRV9TWU5DX1RSSUdHRVJfV0VCSE9PSxADKo8BChZEYXRhc291cmNlRG9tYWluU3RhdHVzEigKJERBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19VTlNQRUNJRklFRBAAEiQKIERBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19QRU5ESU5HEAESJQohREFUQVNPVVJDRV9ET01BSU5fU1RBVFVTX1ZFUklGSUVEEAIqlQMKGlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uEiwKKFNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fVU5TUEVDSUZJRUQQABIoCiRTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1JFVk9LRUQQARIpCiVTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1JFUExBQ0VEEAISLworU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9TT1VSQ0VfREVMRVRFRBADEi8KK1NPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fTUVNQkVSX1JFTU9WRUQQBBIwCixTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1BFUk1JU1NJT05fTE9TVBAFEi4KKlNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fVVNFUl9JTkFDVElWRRAGEjAKLFNPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fQklORElOR19DSEFOR0VEEAcyiTgKEURhdGFzb3VyY2VTZXJ2aWNlErkDChxQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzEjUuc2Fhcy5hY2NvdW50cy52MS5QcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlIqkCwvMYbAgCEAMaEWRhdGFzb3VyY2U6aW52b2tlIhFkYXRhc291cmNlOmludm9rZSoPCglzb3VyY2VfaWQQBBgDMAE6Iwofc2Fhcy5kYXRhc291cmNlLnJlY2VpcHRzLnBydW5lZBACQANIBFADWANgAdrzGLQBCgIIPBIDCNgEGAUiAggCKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMg9zYWFzLWRhdGFzb3VyY2VKRi9zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTZXJ2aWNlL0xvb2t1cFBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNQgCBYgCBgAXIYCgZzb3VyY2USBmludm9rZRIEcmVhZBgBEuYBCiJMb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzEjsuc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlIkvC8xhHCAIQAxoPZGF0YXNvdXJjZTpyZWFkIg9kYXRhc291cmNlOnJlYWQqDwoJZWZmZWN0X2lkEAQYAzABOgIQAUABSANQA1gDYAES/AMKFUludm9rZVNvdXJjZU9wZXJhdGlvbhIuLnNhYXMuYWNjb3VudHMudjEuSW52b2tlU291cmNlT3BlcmF0aW9uUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuSW52b2tlU291cmNlT3BlcmF0aW9uUmVzcG9uc2UigQPC8xhuCAIQAxoRZGF0YXNvdXJjZTppbnZva2UiEWRhdGFzb3VyY2U6aW52b2tlKg8KCXNvdXJjZV9pZBAEGAMwATolCiFzYWFzLmRhdGFzb3VyY2Uub3BlcmF0aW9uLmludm9rZWQQBEADSARQBFgEYAHa8xivAQoCCDwSAwjYBBgFIgIIAioLVU5BVkFJTEFCTEUqElJFU09VUkNFX0VYSEFVU1RFRCoHQUJPUlRFRDIPc2Fhcy1kYXRhc291cmNlSj8vc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlU2VydmljZS9Mb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25QgIAEWICAQGABchgKBnNvdXJjZRIGaW52b2tlEgRyZWFkGAGC0+STAlU6ASoiUC92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMve29wZXJhdGlvbn06aW52b2tlEocCChtMb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb24SNC5zYWFzLmFjY291bnRzLnYxLkxvb2t1cEludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlIoABwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUANYBGABgtPkkwIvEi0vdjEvZGF0YXNvdXJjZS1vcGVyYXRpb24tcmVjZWlwdHMve2VmZmVjdF9pZH0ShQIKFExpc3RTb3VyY2VPcGVyYXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlT3BlcmF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2UijQHC8xhECAIQAxoPZGF0YXNvdXJjZTpyZWFkIg9kYXRhc291cmNlOnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAj8SPS92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMSkwIKF0RlY2xhcmVTb3VyY2VPcGVyYXRpb25zEjAuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1JlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkRlY2xhcmVTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2UikgHC8xhGCAIQBCoMCgZvcmdfaWQQAhgBMAE6Jgoic2Fhcy5kYXRhc291cmNlLm9wZXJhdGlvbi5kZWNsYXJlZBACQAFIBFADWANgAYLT5JMCQjoBKiI9L3YxL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZGF0YXNvdXJjZXMve3NvdXJjZV9pZH0vb3BlcmF0aW9ucxKsAQoPQWRkR2l0SHViU291cmNlEiguc2Fhcy5hY2NvdW50cy52MS5BZGRHaXRIdWJTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5BZGRHaXRIdWJTb3VyY2VSZXNwb25zZSJEwvMYQAgCEAQqDAoGb3JnX2lkEAIYATABOiAKHHNhYXMuZGF0YXNvdXJjZS5zb3VyY2UuYWRkZWQQAkABSARQBFgDYAESmgEKCUFkZFNvdXJjZRIiLnNhYXMuYWNjb3VudHMudjEuQWRkU291cmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuQWRkU291cmNlUmVzcG9uc2UiRMLzGEAIAhAEKgwKBm9yZ19pZBACGAEwATogChxzYWFzLmRhdGFzb3VyY2Uuc291cmNlLmFkZGVkEAJAAUgEUARYA2ABEo8BChRHZXREYXRhc291cmNlQ2F0YWxvZxItLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlQ2F0YWxvZ1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSANQAlgCYAESggEKC0xpc3RTb3VyY2VzEiQuc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VzUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEnwKCUdldFNvdXJjZRIiLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlUmVxdWVzdBojLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEp4BCgpTeW5jU291cmNlEiMuc2Fhcy5hY2NvdW50cy52MS5TeW5jU291cmNlUmVxdWVzdBokLnNhYXMuYWNjb3VudHMudjEuU3luY1NvdXJjZVJlc3BvbnNlIkXC8xhBCAIQBCoMCgZvcmdfaWQQAhgBMAE6IQodc2Fhcy5kYXRhc291cmNlLnNvdXJjZS5zeW5jZWQQAkABSARQBFgCYAESiAEKDUdldFNvdXJjZVN5bmMSJi5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVN5bmNSZXF1ZXN0Gicuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VTeW5jUmVzcG9uc2UiJsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEqUBCgxEZWxldGVTb3VyY2USJS5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZVNvdXJjZVJlcXVlc3QaJi5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZVNvdXJjZVJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLnNvdXJjZS5yZW1vdmVkEAJAAUgEUAJYAmABEsQBChNCZWdpbkdpdEh1YkFwcFNldHVwEiwuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkdpdEh1YkFwcFNldHVwUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlIlDC8xhMCAIQBCoMCgZvcmdfaWQQAhgBMAE6LAooc2Fhcy5kYXRhc291cmNlLmdpdGh1Yl9hcHAuc2V0dXBfc3RhcnRlZBACQAFIBFACWARgARLPAQoWQ29tcGxldGVHaXRIdWJBcHBTZXR1cBIvLnNhYXMuYWNjb3VudHMudjEuQ29tcGxldGVHaXRIdWJBcHBTZXR1cFJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZSJSwvMYTggCEAQqDAoGb3JnX2lkEAIYATABOi4KKnNhYXMuZGF0YXNvdXJjZS5naXRodWJfYXBwLnNldHVwX2NvbXBsZXRlZBACQAFIBFAEWANgARLNAQoYTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwEjEuc2Fhcy5hY2NvdW50cy52MS5NaWdyYXRlR2l0SHViU291cmNlVG9BcHBSZXF1ZXN0GjIuc2Fhcy5hY2NvdW50cy52MS5NaWdyYXRlR2l0SHViU291cmNlVG9BcHBSZXNwb25zZSJKwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5jcmVkZW50aWFsLnVwZGF0ZWQQAkABSARQAlgDYAES1QEKGkJlZ2luRGF0YXNvdXJjZUFjY291bnRMaW5rEjMuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkRhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNC5zYWFzLmFjY291bnRzLnYxLkJlZ2luRGF0YXNvdXJjZUFjY291bnRMaW5rUmVzcG9uc2UiTMLzGEgIAhADKgwKBm9yZ19pZBACGAEwATooCiRzYWFzLmRhdGFzb3VyY2UuYWNjb3VudF9saW5rX3N0YXJ0ZWQQAkABSARQAlgEYAES2AEKHUNvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rEjYuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVzcG9uc2UiRsLzGEIIAhADKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLmRhdGFzb3VyY2UuYWNjb3VudF9saW5rZWQQAkABSARQBFgDYAEStQEKHExpc3RNeURhdGFzb3VyY2VBY2NvdW50TGlua3MSNS5zYWFzLmFjY291bnRzLnYxLkxpc3RNeURhdGFzb3VyY2VBY2NvdW50TGlua3NSZXF1ZXN0GjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2UiJsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABEtQBChtEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmsSNC5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1JlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkjC8xhECAIQAyoMCgZvcmdfaWQQAhgBMAE6JAogc2Fhcy5kYXRhc291cmNlLmFjY291bnRfdW5saW5rZWQQAkABSARQAlgDYAESowEKFkdldERhdGFzb3VyY2VEaXJlY3RvcnkSLy5zYWFzLmFjY291bnRzLnYxLkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlRGlyZWN0b3J5UmVzcG9uc2UiJsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUAJYA2ABErcBChNCaW5kRGF0YXNvdXJjZUdyb3VwEiwuc2Fhcy5hY2NvdW50cy52MS5CaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlIkPC8xg/CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hwobc2Fhcy5kYXRhc291cmNlLmdyb3VwX2JvdW5kEAJAAUgEUAJYA2ABEr8BChVVbmJpbmREYXRhc291cmNlR3JvdXASLi5zYWFzLmFjY291bnRzLnYxLlVuYmluZERhdGFzb3VyY2VHcm91cFJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLlVuYmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlIkXC8xhBCAIQBCoMCgZvcmdfaWQQAhgBMAE6IQodc2Fhcy5kYXRhc291cmNlLmdyb3VwX3VuYm91bmQQAkABSARQAlgDYAESwAEKFUNsYWltRGF0YXNvdXJjZURvbWFpbhIuLnNhYXMuYWNjb3VudHMudjEuQ2xhaW1EYXRhc291cmNlRG9tYWluUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuQ2xhaW1EYXRhc291cmNlRG9tYWluUmVzcG9uc2UiRsLzGEIIAhAEKgwKBm9yZ19pZBACGAEwAToiCh5zYWFzLmRhdGFzb3VyY2UuZG9tYWluX2NsYWltZWQQAkABSARQAlgDYAESxAEKFlZlcmlmeURhdGFzb3VyY2VEb21haW4SLy5zYWFzLmFjY291bnRzLnYxLlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5WZXJpZnlEYXRhc291cmNlRG9tYWluUmVzcG9uc2UiR8LzGEMIAhAEKgwKBm9yZ19pZBACGAEwATojCh9zYWFzLmRhdGFzb3VyY2UuZG9tYWluX3ZlcmlmaWVkEAJAAUgEUAJYA2ABEsMBChZEZWxldGVEYXRhc291cmNlRG9tYWluEi8uc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuRGVsZXRlRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmRvbWFpbl9yZW1vdmVkEAJAAUgEUAJYA2ABEqABChVMaXN0U291cmNlRGVsZWdhdGlvbnMSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VEZWxlZ2F0aW9uc1JlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARLHAQoWUmV2b2tlU291cmNlRGVsZWdhdGlvbhIvLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU291cmNlRGVsZWdhdGlvblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZSJKwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5kZWxlZ2F0aW9uLnJldm9rZWQQAkABSARQAlgDYAES6wEKFkRlbGVnYXRlU291cmNlVHJpZ2dlcnMSLy5zYWFzLmFjY291bnRzLnYxLkRlbGVnYXRlU291cmNlVHJpZ2dlcnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVzcG9uc2UibsLzGGoIAhAEKgwKBm9yZ19pZBACGAEwATpKCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5jcmVhdGVkCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5yZXZva2VkEAJAAUgEUAJYA2ABErIBChtHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb24SNC5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlcXVlc3QaNS5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvblJlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKDAwoPUmVjb25jaWxlU291cmNlEiguc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXNwb25zZSKaAsLzGGoIAhADGhFkYXRhc291cmNlOmludm9rZSIRZGF0YXNvdXJjZTppbnZva2UqDwoJc291cmNlX2lkEAQYAzABOiEKHXNhYXMuZGF0YXNvdXJjZS5zb3VyY2Uuc3luY2VkEAJAA0gEUAJYAmAB2vMYpwEKAgg8EgMI2AQYBSICCAIqC1VOQVZBSUxBQkxFKhJSRVNPVVJDRV9FWEhBVVNURUQqB0FCT1JURUQyD3NhYXMtZGF0YXNvdXJjZUo5L3NhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVNlcnZpY2UvTG9va3VwUmVjb25jaWxlU291cmNlUIAgWIAgYAFyGAoGc291cmNlEgZpbnZva2USBHJlYWQYARK/AQoVTG9va3VwUmVjb25jaWxlU291cmNlEi4uc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBSZWNvbmNpbGVTb3VyY2VSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5SZWNvbmNpbGVTb3VyY2VSZXNwb25zZSJLwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUAJYAmABQrcBChRjb20uc2Fhcy5hY2NvdW50cy52MUIPRGF0YXNvdXJjZVByb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_codefly_runnable_v0_options, file_google_api_annotations, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiFzYWFzL2FjY291bnRzL3YxL2RhdGFzb3VyY2UucHJvdG8SEHNhYXMuYWNjb3VudHMudjEiXgoWR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIMCgRyZXBvGAEgASgJEg0KBXBhdGhzGAIgAygJEg4KBmJyYW5jaBgDIAEoCRIXCg9maWxlX2V4dGVuc2lvbnMYBCADKAki9AEKD0FwaU9BdXRoMkNvbmZpZxI2CgVncmFudBgEIAEoDjInLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnLkdyYW50EhEKCXRva2VuX3VybBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSDgoGc2NvcGVzGAMgAygJInMKBUdyYW50EhUKEUdSQU5UX1VOU1BFQ0lGSUVEEAASFwoTR1JBTlRfUkVGUkVTSF9UT0tFThABEhwKGEdSQU5UX0NMSUVOVF9DUkVERU5USUFMUxACEhwKGEdSQU5UX0FVVEhPUklaQVRJT05fQ09ERRADIuoBChNBcGlEYXRhc291cmNlQ29uZmlnEhAKCGJhc2VfdXJsGAEgASgJEhUKDXJlc291cmNlX3BhdGgYAiABKAkSPAoPY3JlZGVudGlhbF9raW5kGAMgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5BcGlDcmVkZW50aWFsS2luZBIZChFjcmVkZW50aWFsX2hlYWRlchgEIAEoCRIeChZjcmVkZW50aWFsX3F1ZXJ5X3BhcmFtGAUgASgJEjEKBm9hdXRoMhgGIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuQXBpT0F1dGgyQ29uZmlnIkEKF0NyYXdsZXJEYXRhc291cmNlQ29uZmlnEhMKC3NpdGVtYXBfdXJsGAEgASgJEhEKCW1heF9wYWdlcxgCIAEoDSKGAQoWVXBsb2FkRGF0YXNvdXJjZUNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIOCgZyZWdpb24YAiABKAkSDgoGYnVja2V0GAMgASgJEg4KBnByZWZpeBgEIAEoCRIVCg1hY2Nlc3Nfa2V5X2lkGAUgASgJEhMKC21heF9vYmplY3RzGAYgASgNIoMHCgpEYXRhc291cmNlEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRI2Cghwcm92aWRlchgDIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEjgKBmdpdGh1YhgFIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuR2l0SHViRGF0YXNvdXJjZUNvbmZpZxIyCgZzdGF0dXMYBiABKA4yIi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTdGF0dXMSGgoSd2ViaG9va19jb25maWd1cmVkGAcgASgIEi4KCmNyZWF0ZWRfYXQYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDmxhc3Rfc3luY2VkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCgNhcGkYCyABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWcSOgoHY3Jhd2xlchgMIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuQ3Jhd2xlckRhdGFzb3VyY2VDb25maWcSOAoGdXBsb2FkGA0gASgLMiguc2Fhcy5hY2NvdW50cy52MS5VcGxvYWREYXRhc291cmNlQ29uZmlnEhgKEGJvdW5kYXJ5X25vZGVfaWQYDiABKAkSNAoQbGFzdF9pbmdlc3RlZF9hdBgPIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoUbGFzdF9pbmdlc3RlZF9jb21taXQYECABKAkSFQoNc3RhdHVzX3JlYXNvbhgRIAEoCRIWCg5ib3VuZGFyeV9sYWJlbBgSIAEoCRISCgpjb25mb3JtYW50GBMgASgIEhcKD2NvbmZvcm1hbmNlX2dhcBgUIAEoCRI/Cg1saXZlX2RlbGl2ZXJ5GBUgASgOMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlTGl2ZURlbGl2ZXJ5EjUKEnJlY29uY2lsZV9pbnRlcnZhbBgWIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkoECAQQBVIRdGFyZ2V0X2NvbGxlY3Rpb24isgMKFkFkZEdpdEh1YlNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI7CgRyZXBvGAIgASgJQi26SCpyKBADGP8BMiFeW0EtWmEtejAtOV8uLV0rL1tBLVphLXowLTlfLi1dKyQSIAoFcGF0aHMYAyADKAlCEbpIDpIBCxBAIgdyBRABGIAEEhgKBmJyYW5jaBgEIAEoCUIIukgFcgMY/wESJAoQYm91bmRhcnlfbm9kZV9pZBgIIAEoCUIIukgFcgOwAQFIABImChBjb2xsZWN0aW9uX2xhYmVsGAkgASgJQgq6SAdyBRABGP8BSAASHgoMYWNjZXNzX3Rva2VuGAYgASgJQgi6SAVyAxiACBIgCg53ZWJob29rX3NlY3JldBgHIAEoCUIIukgFcgMYgAgSSQoPZmlsZV9leHRlbnNpb25zGAogAygJQjC6SC2SASoQICImciQQAhghMh5eXC5bQS1aYS16MC05XVtBLVphLXowLTkuXy1dKiRCEQoIYm91bmRhcnkSBbpIAggBSgQIBRAGUhF0YXJnZXRfY29sbGVjdGlvbiJLChdBZGRHaXRIdWJTb3VyY2VSZXNwb25zZRIwCgpkYXRhc291cmNlGAEgASgLMhwuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlIsgEChBBZGRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESQgoIcHJvdmlkZXIYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckIKukgHggEEEAEgABI6CgZnaXRodWIYBCABKAsyKC5zYWFzLmFjY291bnRzLnYxLkdpdEh1YkRhdGFzb3VyY2VDb25maWdIABI0CgNhcGkYBSABKAsyJS5zYWFzLmFjY291bnRzLnYxLkFwaURhdGFzb3VyY2VDb25maWdIABI8CgdjcmF3bGVyGAggASgLMikuc2Fhcy5hY2NvdW50cy52MS5DcmF3bGVyRGF0YXNvdXJjZUNvbmZpZ0gAEjoKBnVwbG9hZBgJIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuVXBsb2FkRGF0YXNvdXJjZUNvbmZpZ0gAEiQKEGJvdW5kYXJ5X25vZGVfaWQYDCABKAlCCLpIBXIDsAEBSAESJgoQY29sbGVjdGlvbl9sYWJlbBgNIAEoCUIKukgHcgUQARj/AUgBEhwKCmNyZWRlbnRpYWwYBiABKAlCCLpIBXIDGIAgEiAKDndlYmhvb2tfc2VjcmV0GAcgASgJQgi6SAVyAxiACBImChRvYXV0aDJfY2xpZW50X3NlY3JldBgKIAEoCUIIukgFcgMYgAhCCAoGY29uZmlnQhEKCGJvdW5kYXJ5EgW6SAIIAUoECAMQBFIRdGFyZ2V0X2NvbGxlY3Rpb24iRQoRQWRkU291cmNlUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJaChVEYXRhc291cmNlQ29uZmlnRmllbGQSCwoDa2V5GAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIMCgRoZWxwGAMgASgJEhAKCHJlcXVpcmVkGAQgASgIIsQBChlEYXRhc291cmNlQ29ubmVjdG9yQnVkZ2V0EhoKEm1heF9pdGVtc19wZXJfY2FsbBgBIAEoDRIaChJtYXhfYnl0ZXNfcGVyX2NhbGwYAiABKAMSFgoObWF4X2l0ZW1fYnl0ZXMYAyABKAMSHQoVb3BlcmF0aW9uc19wZXJfd2luZG93GAQgASgNEhYKDndpbmRvd19zZWNvbmRzGAUgASgDEiAKGGJhY2tncm91bmRfc2hhcmVfcGVyY2VudBgGIAEoDSKqBgocRGF0YXNvdXJjZVByb3ZpZGVyRGVzY3JpcHRvchI2Cghwcm92aWRlchgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVByb3ZpZGVyEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRI+Cg1jb25maWdfZmllbGRzGAQgAygLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ29uZmlnRmllbGQSGAoQc3VwcG9ydHNfd2ViaG9vaxgFIAEoCBJHChpzdXBwb3J0ZWRfY3JlZGVudGlhbF9raW5kcxgGIAMoDjIjLnNhYXMuYWNjb3VudHMudjEuQXBpQ3JlZGVudGlhbEtpbmQSEQoJY29ubmVjdG9yGAcgASgJEkEKCWludGVyZmFjZRgIIAEoDjIuLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRJEChBjcmVkZW50aWFsX21vZGVzGAkgAygOMiouc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQ3JlZGVudGlhbE1vZGUSPwoNcmVhZGVyc19tb2RlbBgKIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVJlYWRlcnNNb2RlbBI7CgZidWRnZXQYCyABKAsyKy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VDb25uZWN0b3JCdWRnZXQSEgoKY29uZm9ybWFudBgMIAEoCBIXCg9jb25mb3JtYW5jZV9nYXAYDSABKAkSGwoTYWNjZXB0c19uZXdfc291cmNlcxgOIAEoCBIgChhsaXZlX2RlbGl2ZXJ5X2NvbmZpZ3VyZWQYDyABKAgSGgoSYWNjZXB0c19vcGVyYXRpb25zGBAgASgIEhYKDm9wZXJhdGlvbnNfZ2FwGBEgASgJEhsKE29hdXRoX2F1dGhvcml6ZV91cmwYEiABKAkSFwoPb2F1dGhfdG9rZW5fdXJsGBMgASgJEhQKDG9hdXRoX3Njb3BlcxgUIAMoCSIdChtHZXREYXRhc291cmNlQ2F0YWxvZ1JlcXVlc3QiYQocR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZRJBCglwcm92aWRlcnMYASADKAsyLi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VQcm92aWRlckRlc2NyaXB0b3IiLgoSTGlzdFNvdXJjZXNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiSAoTTGlzdFNvdXJjZXNSZXNwb25zZRIxCgtkYXRhc291cmNlcxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSJCChBHZXRTb3VyY2VSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIkUKEUdldFNvdXJjZVJlc3BvbnNlEjAKCmRhdGFzb3VyY2UYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2UidgoRU3luY1NvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQESHgoMYWNjZXNzX3Rva2VuGAMgASgJQgi6SAVyAxiAIBIRCglyZWNvbm5lY3QYBCABKAgiJAoSU3luY1NvdXJjZVJlc3BvbnNlEg4KBmpvYl9pZBgBIAEoCSJqChRHZXRTb3VyY2VTeW5jUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESGwoGam9iX2lkGAMgASgJQgu6SAjYAQFyA7ABASKVAQoSU291cmNlU3luY0RlbGl2ZXJ5EhgKBmpvYl9pZBgBIAEoCUIIukgFcgOwAQESLQoFc3RhdGUYAiABKA4yFi5zYWFzLmpvYnMudjEuSm9iU3RhdGVCBrpIA8gBARI2CglleGVjdXRpb24YAyABKAsyIy5zYWFzLmpvYnMudjEuSm9iRXhlY3V0aW9uUmVmZXJlbmNlIosBChFTb3VyY2VTeW5jQ2hhbmdlcxINCgVmaWxlcxgBIAEoDRINCgVhZGRlZBgCIAEoDRIQCghtb2RpZmllZBgDIAEoDRIPCgdkZWxldGVkGAQgASgNEhMKC3NwbGl0X2tub3duGAUgASgIEhAKCHNuYXBzaG90GAYgASgIEg4KBmNvbW1pdBgHIAEoCSKtAQoRU291cmNlU3luY0ZhaWx1cmUSOQoGcmVhc29uGAEgASgOMikuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jRmFpbHVyZVJlYXNvbhIMCgRjb2RlGAIgASgJEg8KB21lc3NhZ2UYAyABKAkSEAoIcmV0cnlpbmcYBCABKAgSLAoIcmV0cnlfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoQEChJTb3VyY2VTeW5jUHJvZ3Jlc3MSMAoFcGhhc2UYASABKA4yIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNQaGFzZRI0Cgd0cmlnZ2VyGAIgASgOMiMuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VTeW5jVHJpZ2dlchItCglxdWV1ZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2ZldGNoaW5nX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtjb21waWxlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMQoNaGFuZGVkX29mZl9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZmluaXNoZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjQKB2NoYW5nZXMYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNDaGFuZ2VzEjQKB2ZhaWx1cmUYCSABKAsyIy5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNGYWlsdXJlEg8KB2F0dGVtcHQYCiABKA0SFAoMbWF4X2F0dGVtcHRzGAsgASgNItIBChVHZXRTb3VyY2VTeW5jUmVzcG9uc2USGAoGam9iX2lkGAEgASgJQgi6SAVyA7ABARItCgVzdGF0ZRgCIAEoDjIWLnNhYXMuam9icy52MS5Kb2JTdGF0ZUIGukgDyAEBEjgKCmRlbGl2ZXJpZXMYAyADKAsyJC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVN5bmNEZWxpdmVyeRI2Cghwcm9ncmVzcxgEIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuU291cmNlU3luY1Byb2dyZXNzIkUKE0RlbGV0ZVNvdXJjZVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiFgoURGVsZXRlU291cmNlUmVzcG9uc2UiVgoTR2l0SHViQXBwUmVwb3NpdG9yeRIMCgRyZXBvGAEgASgJEhYKDmRlZmF1bHRfYnJhbmNoGAIgASgJEhkKEWFscmVhZHlfY29ubmVjdGVkGAMgASgIIjYKGkJlZ2luR2l0SHViQXBwU2V0dXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEicQobQmVnaW5HaXRIdWJBcHBTZXR1cFJlc3BvbnNlEhMKC2luc3RhbGxfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIpwBCh1Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGP8BEiwKD2luc3RhbGxhdGlvbl9pZBgDIAEoCUITukgQcg4QARggMgheWzAtOV0rJBIYCgRjb2RlGAQgASgJQgq6SAdyBRABGP8BInYKHkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXNwb25zZRIXCg9pbnN0YWxsYXRpb25faWQYASABKAkSOwoMcmVwb3NpdG9yaWVzGAIgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5HaXRIdWJBcHBSZXBvc2l0b3J5IlEKH01pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIUCgJpZBgCIAEoCUIIukgFcgOwAQEiVAogTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2USMAoKZGF0YXNvdXJjZRgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZSLEAQoVRGF0YXNvdXJjZUFjY291bnRMaW5rEgoKAmlkGAEgASgJEg4KBm9yZ19pZBgCIAEoCRIPCgd1c2VyX2lkGAMgASgJEhEKCWNvbm5lY3RvchgEIAEoCRIbChNwcm92aWRlcl9hY2NvdW50X2lkGAUgASgJEh4KFnByb3ZpZGVyX2FjY291bnRfbG9naW4YBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAitwEKFkRhdGFzb3VyY2VHcm91cEJpbmRpbmcSCgoCaWQYASABKAkSDgoGb3JnX2lkGAIgASgJEhEKCWNvbm5lY3RvchgDIAEoCRIZChFwcm92aWRlcl9ncm91cF9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJEhIKCmNyZWF0ZWRfYnkYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilAIKGERhdGFzb3VyY2VWZXJpZmllZERvbWFpbhIKCgJpZBgBIAEoCRIOCgZvcmdfaWQYAiABKAkSDgoGZG9tYWluGAMgASgJEjgKBnN0YXR1cxgEIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURvbWFpblN0YXR1cxIXCg90eHRfcmVjb3JkX25hbWUYBSABKAkSGAoQdHh0X3JlY29yZF92YWx1ZRgGIAEoCRIvCgt2ZXJpZmllZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKY3JlYXRlZF9hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiMwoXRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCSKmAQohQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSHgoMcmVkaXJlY3RfdXJpGAMgASgJQgi6SAVyAxiAEBIRCglzb3VyY2VfaWQYBCABKAkiegoiQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRIVCg1hdXRob3JpemVfdXJsGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmV4cGlyZXNfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInUKJENvbXBsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhkKBXN0YXRlGAIgASgJQgq6SAdyBRABGIBAEhgKBGNvZGUYAyABKAlCCrpIB3IFEAEY/wEiXgolQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZRI1CgRsaW5rGAEgASgLMicuc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlQWNjb3VudExpbmsiPwojTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJeCiRMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVzcG9uc2USNgoFbGlua3MYASADKAsyJy5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VBY2NvdW50TGluayJUCiJEZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIiUKI0RlbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIjkKHUdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiiwIKHkdldERhdGFzb3VyY2VEaXJlY3RvcnlSZXNwb25zZRI2CgVsaW5rcxgBIAMoCzInLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZUFjY291bnRMaW5rEjoKCGJpbmRpbmdzGAIgAygLMiguc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlR3JvdXBCaW5kaW5nEjsKB2RvbWFpbnMYAyADKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbhI4CgV0ZWFtcxgEIAMoCzIpLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZURpcmVjdG9yeVRlYW0irgEKGkJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoJY29ubmVjdG9yGAIgASgJQiG6SB5yHBACGCAyFl5bYS16XVthLXowLTktXXsxLDMxfSQSJQoRcHJvdmlkZXJfZ3JvdXBfaWQYAyABKAlCCrpIB3IFEAEY/wESGQoHdGVhbV9pZBgEIAEoCUIIukgFcgOwAQEiWAobQmluZERhdGFzb3VyY2VHcm91cFJlc3BvbnNlEjkKB2JpbmRpbmcYASABKAsyKC5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VHcm91cEJpbmRpbmciTgocVW5iaW5kRGF0YXNvdXJjZUdyb3VwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIfCh1VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJUChxDbGFpbURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGgoGZG9tYWluGAIgASgJQgq6SAdyBRADGP0BIlsKHUNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlEjoKBmRvbWFpbhgBIAEoCzIqLnNhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVZlcmlmaWVkRG9tYWluIk8KHVZlcmlmeURhdGFzb3VyY2VEb21haW5SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlwKHlZlcmlmeURhdGFzb3VyY2VEb21haW5SZXNwb25zZRI6CgZkb21haW4YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VWZXJpZmllZERvbWFpbiJPCh1EZWxldGVEYXRhc291cmNlRG9tYWluUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhQKAmlkGAIgASgJQgi6SAVyA7ABASIgCh5EZWxldGVEYXRhc291cmNlRG9tYWluUmVzcG9uc2UingIKEFNvdXJjZURlbGVnYXRpb24SCgoCaWQYASABKAkSEQoJc291cmNlX2lkGAIgASgJEhQKDHByaW5jaXBhbF9pZBgDIAEoCRIOCgZtb2R1bGUYBCABKAkSDwoHYmluZGluZxgFIAEoCRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpyZXZva2VkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBJACgpyZXZvY2F0aW9uGAggASgOMiwuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhISCgpyZXZva2VkX2J5GAkgASgJIoEBChxMaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIAoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABAUgAiAEBEhcKD2luY2x1ZGVfcmV2b2tlZBgDIAEoCEIMCgpfc291cmNlX2lkIlgKHUxpc3RTb3VyY2VEZWxlZ2F0aW9uc1Jlc3BvbnNlEjcKC2RlbGVnYXRpb25zGAEgAygLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIk8KHVJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESFAoCaWQYAiABKAlCCLpIBXIDsAEBIlgKHlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXNwb25zZRI2CgpkZWxlZ2F0aW9uGAEgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5Tb3VyY2VEZWxlZ2F0aW9uIjIKD1RyaWdnZXJJZGVudGl0eRIOCgZzb3VyY2UYASABKAkSDwoHYmluZGluZxgCIAEoCSKEAQoXRGVjbGFyZWRUcmlnZ2VyU2NoZWR1bGUSDAoEY3JvbhgBIAEoCRIQCgh0aW1lem9uZRgCIAEoCRISCgptaXNzZWRfcnVuGAMgASgJEg8KB292ZXJsYXAYBCABKAkSEAoIZGVhZGxpbmUYBSABKAMSEgoKaW5wdXRfanNvbhgGIAEoCSLnAgoaVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USEQoJcmVmZXJlbmNlGAEgASgJEhUKDWRlbGVnYXRpb25faWQYAiABKAkSDgoGbW9kdWxlGAMgASgJEg8KB2JpbmRpbmcYBCABKAkSEAoIYXVkaWVuY2UYBSABKAkSDgoGdGVuYW50GAYgASgJEhoKEm93bmVyX3ByaW5jaXBhbF9pZBgHIAEoCRIOCgZzY29wZXMYCCADKAkSDgoGYWN0aXZlGAkgASgIEi4KCmNyZWF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnJldm9rZWRfYXQYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEkAKCnJldm9jYXRpb24YDCABKA4yLC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZURlbGVnYXRpb25SZXZvY2F0aW9uIr4BChNUcmlnZ2VyQ2hpbGRDb250ZXh0EhUKDWRlbGVnYXRpb25faWQYASABKAkSDgoGbW9kdWxlGAIgASgJEg8KB2JpbmRpbmcYAyABKAkSEAoIYXVkaWVuY2UYBCABKAkSDgoGc2NvcGVzGAUgAygJEg4KBmxvb2t1cBgGIAEoCBItCgltaW50ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBnJlY29yZBgIIAEoCSK5AwoYU291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEg8KB3JlbGVhc2UYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEjsKCHNjaGVkdWxlGAMgASgLMikuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlZFRyaWdnZXJTY2hlZHVsZRIzCghpZGVudGl0eRgEIAEoCzIhLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlcklkZW50aXR5Eg4KBmRpZ2VzdBgFIAEoCRI2ChJsYXN0X29jY3VycmVuY2VfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjYKEm5leHRfb2NjdXJyZW5jZV9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTdW5kZWNsYXJhYmxlX3JlYXNvbhgIIAEoCRIaChJob3N0X3N3ZWVwX2VuYWJsZWQYCSABKAgSNQoRaG9zdF9zd2VlcF9kdWVfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKD2lkZW50aXR5X2hvbGRlchgLIAEoCSJWCh1EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQEiYwoeRGVsZWdhdGVTb3VyY2VUcmlnZ2Vyc1Jlc3BvbnNlEkEKC2RlbGVnYXRpb25zGAEgAygLMiwuc2Fhcy5hY2NvdW50cy52MS5UcmlnZ2VyRGVsZWdhdGlvblJlZmVyZW5jZSJbCiJHZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABASLiAQojR2V0U291cmNlVHJpZ2dlckRlY2xhcmF0aW9uUmVzcG9uc2USPwoLZGVjbGFyYXRpb24YASABKAsyKi5zYWFzLmFjY291bnRzLnYxLlNvdXJjZVRyaWdnZXJEZWNsYXJhdGlvbhJBCgtkZWxlZ2F0aW9ucxgCIAMoCzIsLnNhYXMuYWNjb3VudHMudjEuVHJpZ2dlckRlbGVnYXRpb25SZWZlcmVuY2USNwoIY2hpbGRyZW4YAyADKAsyJS5zYWFzLmFjY291bnRzLnYxLlRyaWdnZXJDaGlsZENvbnRleHQibgoWUmVjb25jaWxlU291cmNlUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEhsKCXNvdXJjZV9pZBgCIAEoCUIIukgFcgOwAQESHQoJZWZmZWN0X2lkGAMgASgJQgq6SAdyBRABGIABIkwKF1JlY29uY2lsZVNvdXJjZVJlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIOCgZzdGF0dXMYAiABKAkSDgoGam9iX2lkGAMgASgJIj0KHExvb2t1cFJlY29uY2lsZVNvdXJjZVJlcXVlc3QSHQoJZWZmZWN0X2lkGAEgASgJQgq6SAdyBRABGIABIvECCg9Tb3VyY2VPcGVyYXRpb24SDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIOCgZtZXRob2QYAyABKAkSDAoEcGF0aBgEIAEoCRINCgVxdWVyeRgFIAMoCRItCgxpbnB1dF9zY2hlbWEYBiABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0Ei4KDW91dHB1dF9zY2hlbWEYByABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EjgKBmVmZmVjdBgIIAEoDjIoLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uLkVmZmVjdBIYChBtYXhfb3V0cHV0X2J5dGVzGAkgASgNEg4KBmRpZ2VzdBgKIAEoCSJLCgZFZmZlY3QSFgoSRUZGRUNUX1VOU1BFQ0lGSUVEEAASFAoQRUZGRUNUX1JFQURfT05MWRABEhMKD0VGRkVDVF9NVVRBVElPThACIo4BCh5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEjUKCm9wZXJhdGlvbnMYAyADKAsyIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvbiJYCh9EZWNsYXJlU291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlEjUKCm9wZXJhdGlvbnMYASADKAsyIS5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvbiJUChtMaXN0U291cmNlT3BlcmF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBIlUKHExpc3RTb3VyY2VPcGVyYXRpb25zUmVzcG9uc2USNQoKb3BlcmF0aW9ucxgBIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuU291cmNlT3BlcmF0aW9uIsMBChxJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGwoJc291cmNlX2lkGAIgASgJQgi6SAVyA7ABARIwCglvcGVyYXRpb24YAyABKAlCHbpIGnIYMhZeW2Etel1bYS16MC05X117MCw2M30kEh0KCmlucHV0X2pzb24YBCABKAlCCbpIBnIEKICABBIbCgllZmZlY3RfaWQYBSABKAlCCLpIBXIDKIABIn8KFlNvdXJjZU9wZXJhdGlvblJlY2VpcHQSEQoJZWZmZWN0X2lkGAEgASgJEhQKDGNvbW1pdHRlZF9hdBgCIAEoCRIOCgZzdGF0dXMYAyABKAkSFwoPcHJvdmlkZXJfc3RhdHVzGAQgASgNEhMKC291dHB1dF9qc29uGAUgASgJIm8KHUludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlEhMKC291dHB1dF9qc29uGAEgASgJEjkKB3JlY2VpcHQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLlNvdXJjZU9wZXJhdGlvblJlY2VpcHQiQwoiTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uUmVxdWVzdBIdCgllZmZlY3RfaWQYASABKAlCCrpIB3IFEAEogAEieQojUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCglzb3VyY2VfaWQYAiABKAlCCLpIBXIDsAEBEhsKCWVmZmVjdF9pZBgDIAEoCUIIukgFcgMogAEiWwokUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1Jlc3BvbnNlEhEKCWVmZmVjdF9pZBgBIAEoCRIQCghyZWNlaXB0cxgCIAEoAxIOCgZzdGF0dXMYAyABKAkiSgopTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QSHQoJZWZmZWN0X2lkGAEgASgJQgq6SAdyBRABKIABKrcBChJEYXRhc291cmNlUHJvdmlkZXISIwofREFUQVNPVVJDRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEh4KGkRBVEFTT1VSQ0VfUFJPVklERVJfR0lUSFVCEAESGwoXREFUQVNPVVJDRV9QUk9WSURFUl9BUEkQAhIfChtEQVRBU09VUkNFX1BST1ZJREVSX0NSQVdMRVIQAxIeChpEQVRBU09VUkNFX1BST1ZJREVSX1VQTE9BRBAEKpEBChBEYXRhc291cmNlU3RhdHVzEiEKHURBVEFTT1VSQ0VfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHAoYREFUQVNPVVJDRV9TVEFUVVNfQUNUSVZFEAESHAoYREFUQVNPVVJDRV9TVEFUVVNfUEFVU0VEEAISHgoaREFUQVNPVVJDRV9TVEFUVVNfREVHUkFERUQQAyq8AQoWRGF0YXNvdXJjZUxpdmVEZWxpdmVyeRIoCiREQVRBU09VUkNFX0xJVkVfREVMSVZFUllfVU5TUEVDSUZJRUQQABIhCh1EQVRBU09VUkNFX0xJVkVfREVMSVZFUllfTk9ORRABEisKJ0RBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9TT1VSQ0VfV0VCSE9PSxACEigKJERBVEFTT1VSQ0VfTElWRV9ERUxJVkVSWV9BUFBfV0VCSE9PSxADKtYBChFBcGlDcmVkZW50aWFsS2luZBIjCh9BUElfQ1JFREVOVElBTF9LSU5EX1VOU1BFQ0lGSUVEEAASHgoaQVBJX0NSRURFTlRJQUxfS0lORF9CRUFSRVIQARIdChlBUElfQ1JFREVOVElBTF9LSU5EX0JBU0lDEAISHgoaQVBJX0NSRURFTlRJQUxfS0lORF9IRUFERVIQAxIdChlBUElfQ1JFREVOVElBTF9LSU5EX1FVRVJZEAQSHgoaQVBJX0NSRURFTlRJQUxfS0lORF9PQVVUSDIQBSqmAgocRGF0YXNvdXJjZUNvbm5lY3RvckludGVyZmFjZRIuCipEQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfVU5TUEVDSUZJRUQQABIoCiREQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfRklMRVMQARIoCiREQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfUEFHRVMQAhIqCiZEQVRBU09VUkNFX0NPTk5FQ1RPUl9JTlRFUkZBQ0VfUkVDT1JEUxADEisKJ0RBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9NRVNTQUdFUxAEEikKJURBVEFTT1VSQ0VfQ09OTkVDVE9SX0lOVEVSRkFDRV9FVkVOVFMQBSrsAQoYRGF0YXNvdXJjZUNyZWRlbnRpYWxNb2RlEioKJkRBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX1VOU1BFQ0lGSUVEEAASIwofREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfTk9ORRABEiYKIkRBVEFTT1VSQ0VfQ1JFREVOVElBTF9NT0RFX09SR19BUFAQAhIpCiVEQVRBU09VUkNFX0NSRURFTlRJQUxfTU9ERV9VU0VSX09BVVRIEAMSLAooREFUQVNPVVJDRV9DUkVERU5USUFMX01PREVfU1RBVElDX1NFQ1JFVBAEKpcBChZEYXRhc291cmNlUmVhZGVyc01vZGVsEigKJERBVEFTT1VSQ0VfUkVBREVSU19NT0RFTF9VTlNQRUNJRklFRBAAEioKJkRBVEFTT1VSQ0VfUkVBREVSU19NT0RFTF9TT1VSQ0VfU0NPUEVEEAESJwojREFUQVNPVVJDRV9SRUFERVJTX01PREVMX1RSQU5TTEFURUQQAiruAQoPU291cmNlU3luY1BoYXNlEiEKHVNPVVJDRV9TWU5DX1BIQVNFX1VOU1BFQ0lGSUVEEAASHAoYU09VUkNFX1NZTkNfUEhBU0VfUVVFVUVEEAESHgoaU09VUkNFX1NZTkNfUEhBU0VfRkVUQ0hJTkcQAhIeChpTT1VSQ0VfU1lOQ19QSEFTRV9DT01QSUxFRBADEiAKHFNPVVJDRV9TWU5DX1BIQVNFX0hBTkRFRF9PRkYQBBIaChZTT1VSQ0VfU1lOQ19QSEFTRV9ET05FEAUSHAoYU09VUkNFX1NZTkNfUEhBU0VfRkFJTEVEEAYqpgMKF1NvdXJjZVN5bmNGYWlsdXJlUmVhc29uEioKJlNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX1VOU1BFQ0lGSUVEEAASKwonU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fUkFURV9MSU1JVEVEEAESKQolU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fQ1JFREVOVElBTBACEiwKKFNPVVJDRV9TWU5DX0ZBSUxVUkVfUkVBU09OX0FDQ0VTU19ERU5JRUQQAxIoCiRTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9OT1RfRk9VTkQQBBIoCiRTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9UT09fTEFSR0UQBRIvCitTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9IT1NUX1VOQVZBSUxBQkxFEAYSJAogU09VUkNFX1NZTkNfRkFJTFVSRV9SRUFTT05fT1RIRVIQBxIuCipTT1VSQ0VfU1lOQ19GQUlMVVJFX1JFQVNPTl9ERUxJVkVSWV9GQUlMRUQQCCqcAQoRU291cmNlU3luY1RyaWdnZXISIwofU09VUkNFX1NZTkNfVFJJR0dFUl9VTlNQRUNJRklFRBAAEh4KGlNPVVJDRV9TWU5DX1RSSUdHRVJfTUFOVUFMEAESIQodU09VUkNFX1NZTkNfVFJJR0dFUl9TQ0hFRFVMRUQQAhIfChtTT1VSQ0VfU1lOQ19UUklHR0VSX1dFQkhPT0sQAyqPAQoWRGF0YXNvdXJjZURvbWFpblN0YXR1cxIoCiREQVRBU09VUkNFX0RPTUFJTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIkCiBEQVRBU09VUkNFX0RPTUFJTl9TVEFUVVNfUEVORElORxABEiUKIURBVEFTT1VSQ0VfRE9NQUlOX1NUQVRVU19WRVJJRklFRBACKpUDChpTb3VyY2VEZWxlZ2F0aW9uUmV2b2NhdGlvbhIsCihTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1VOU1BFQ0lGSUVEEAASKAokU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9SRVZPS0VEEAESKQolU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9SRVBMQUNFRBACEi8KK1NPVVJDRV9ERUxFR0FUSU9OX1JFVk9DQVRJT05fU09VUkNFX0RFTEVURUQQAxIvCitTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX01FTUJFUl9SRU1PVkVEEAQSMAosU09VUkNFX0RFTEVHQVRJT05fUkVWT0NBVElPTl9QRVJNSVNTSU9OX0xPU1QQBRIuCipTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX1VTRVJfSU5BQ1RJVkUQBhIwCixTT1VSQ0VfREVMRUdBVElPTl9SRVZPQ0FUSU9OX0JJTkRJTkdfQ0hBTkdFRBAHMok4ChFEYXRhc291cmNlU2VydmljZRK5AwocUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0cxI1LnNhYXMuYWNjb3VudHMudjEuUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLlBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNSZXNwb25zZSKpAsLzGGwIAhADGhFkYXRhc291cmNlOmludm9rZSIRZGF0YXNvdXJjZTppbnZva2UqDwoJc291cmNlX2lkEAQYAzABOiMKH3NhYXMuZGF0YXNvdXJjZS5yZWNlaXB0cy5wcnVuZWQQAkADSARQA1gDYAHa8xi0AQoCCDwSAwjYBBgFIgIIAioLVU5BVkFJTEFCTEUqElJFU09VUkNFX0VYSEFVU1RFRCoHQUJPUlRFRDIPc2Fhcy1kYXRhc291cmNlSkYvc2Fhcy5hY2NvdW50cy52MS5EYXRhc291cmNlU2VydmljZS9Mb29rdXBQcnVuZVNvdXJjZU9wZXJhdGlvblJlY2VpcHRzUIAgWIAgYAFyGAoGc291cmNlEgZpbnZva2USBHJlYWQYARLmAQoiTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0cxI7LnNhYXMuYWNjb3VudHMudjEuTG9va3VwUHJ1bmVTb3VyY2VPcGVyYXRpb25SZWNlaXB0c1JlcXVlc3QaNi5zYWFzLmFjY291bnRzLnYxLlBydW5lU291cmNlT3BlcmF0aW9uUmVjZWlwdHNSZXNwb25zZSJLwvMYRwgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKg8KCWVmZmVjdF9pZBAEGAMwAToCEAFAAUgDUANYA2ABEvwDChVJbnZva2VTb3VyY2VPcGVyYXRpb24SLi5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkludm9rZVNvdXJjZU9wZXJhdGlvblJlc3BvbnNlIoEDwvMYbggCEAMaEWRhdGFzb3VyY2U6aW52b2tlIhFkYXRhc291cmNlOmludm9rZSoPCglzb3VyY2VfaWQQBBgDMAE6JQohc2Fhcy5kYXRhc291cmNlLm9wZXJhdGlvbi5pbnZva2VkEARAA0gEUARYBGAB2vMYrwEKAgg8EgMI2AQYBSICCAIqC1VOQVZBSUxBQkxFKhJSRVNPVVJDRV9FWEhBVVNURUQqB0FCT1JURUQyD3NhYXMtZGF0YXNvdXJjZUo/L3NhYXMuYWNjb3VudHMudjEuRGF0YXNvdXJjZVNlcnZpY2UvTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uUICABFiAgEBgAXIYCgZzb3VyY2USBmludm9rZRIEcmVhZBgBgtPkkwJVOgEqIlAvdjEvb3JnYW5pemF0aW9ucy97b3JnX2lkfS9kYXRhc291cmNlcy97c291cmNlX2lkfS9vcGVyYXRpb25zL3tvcGVyYXRpb259Omludm9rZRKHAgobTG9va3VwSW52b2tlU291cmNlT3BlcmF0aW9uEjQuc2Fhcy5hY2NvdW50cy52MS5Mb29rdXBJbnZva2VTb3VyY2VPcGVyYXRpb25SZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5JbnZva2VTb3VyY2VPcGVyYXRpb25SZXNwb25zZSKAAcLzGEcIAhADGg9kYXRhc291cmNlOnJlYWQiD2RhdGFzb3VyY2U6cmVhZCoPCgllZmZlY3RfaWQQBBgDMAE6AhABQAFIA1ADWARgAYLT5JMCLxItL3YxL2RhdGFzb3VyY2Utb3BlcmF0aW9uLXJlY2VpcHRzL3tlZmZlY3RfaWR9EoUCChRMaXN0U291cmNlT3BlcmF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdFNvdXJjZU9wZXJhdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlIo0BwvMYRAgCEAMaD2RhdGFzb3VyY2U6cmVhZCIPZGF0YXNvdXJjZTpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwI/Ej0vdjEvb3JnYW5pemF0aW9ucy97b3JnX2lkfS9kYXRhc291cmNlcy97c291cmNlX2lkfS9vcGVyYXRpb25zEpMCChdEZWNsYXJlU291cmNlT3BlcmF0aW9ucxIwLnNhYXMuYWNjb3VudHMudjEuRGVjbGFyZVNvdXJjZU9wZXJhdGlvbnNSZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5EZWNsYXJlU291cmNlT3BlcmF0aW9uc1Jlc3BvbnNlIpIBwvMYRggCEAQqDAoGb3JnX2lkEAIYATABOiYKInNhYXMuZGF0YXNvdXJjZS5vcGVyYXRpb24uZGVjbGFyZWQQAkABSARQA1gDYAGC0+STAkI6ASoiPS92MS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2RhdGFzb3VyY2VzL3tzb3VyY2VfaWR9L29wZXJhdGlvbnMSrAEKD0FkZEdpdEh1YlNvdXJjZRIoLnNhYXMuYWNjb3VudHMudjEuQWRkR2l0SHViU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuQWRkR2l0SHViU291cmNlUmVzcG9uc2UiRMLzGEAIAhAEKgwKBm9yZ19pZBACGAEwATogChxzYWFzLmRhdGFzb3VyY2Uuc291cmNlLmFkZGVkEAJAAUgEUARYA2ABEpoBCglBZGRTb3VyY2USIi5zYWFzLmFjY291bnRzLnYxLkFkZFNvdXJjZVJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLkFkZFNvdXJjZVJlc3BvbnNlIkTC8xhACAIQBCoMCgZvcmdfaWQQAhgBMAE6IAocc2Fhcy5kYXRhc291cmNlLnNvdXJjZS5hZGRlZBACQAFIBFAEWANgARKPAQoUR2V0RGF0YXNvdXJjZUNhdGFsb2cSLS5zYWFzLmFjY291bnRzLnYxLkdldERhdGFzb3VyY2VDYXRhbG9nUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZUNhdGFsb2dSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgDUAJYAmABEoIBCgtMaXN0U291cmNlcxIkLnNhYXMuYWNjb3VudHMudjEuTGlzdFNvdXJjZXNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlc1Jlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARJ8CglHZXRTb3VyY2USIi5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVJlcXVlc3QaIy5zYWFzLmFjY291bnRzLnYxLkdldFNvdXJjZVJlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKeAQoKU3luY1NvdXJjZRIjLnNhYXMuYWNjb3VudHMudjEuU3luY1NvdXJjZVJlcXVlc3QaJC5zYWFzLmFjY291bnRzLnYxLlN5bmNTb3VyY2VSZXNwb25zZSJFwvMYQQgCEAQqDAoGb3JnX2lkEAIYATABOiEKHXNhYXMuZGF0YXNvdXJjZS5zb3VyY2Uuc3luY2VkEAJAAUgEUARYAmABEogBCg1HZXRTb3VyY2VTeW5jEiYuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VTeW5jUmVxdWVzdBonLnNhYXMuYWNjb3VudHMudjEuR2V0U291cmNlU3luY1Jlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARKlAQoMRGVsZXRlU291cmNlEiUuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVTb3VyY2VSZXF1ZXN0GiYuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVTb3VyY2VSZXNwb25zZSJGwvMYQggCEAQqDAoGb3JnX2lkEAIYATABOiIKHnNhYXMuZGF0YXNvdXJjZS5zb3VyY2UucmVtb3ZlZBACQAFIBFACWAJgARLEAQoTQmVnaW5HaXRIdWJBcHBTZXR1cBIsLnNhYXMuYWNjb3VudHMudjEuQmVnaW5HaXRIdWJBcHBTZXR1cFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkJlZ2luR2l0SHViQXBwU2V0dXBSZXNwb25zZSJQwvMYTAgCEAQqDAoGb3JnX2lkEAIYATABOiwKKHNhYXMuZGF0YXNvdXJjZS5naXRodWJfYXBwLnNldHVwX3N0YXJ0ZWQQAkABSARQAlgEYAESzwEKFkNvbXBsZXRlR2l0SHViQXBwU2V0dXASLy5zYWFzLmFjY291bnRzLnYxLkNvbXBsZXRlR2l0SHViQXBwU2V0dXBSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZUdpdEh1YkFwcFNldHVwUmVzcG9uc2UiUsLzGE4IAhAEKgwKBm9yZ19pZBACGAEwATouCipzYWFzLmRhdGFzb3VyY2UuZ2l0aHViX2FwcC5zZXR1cF9jb21wbGV0ZWQQAkABSARQBFgDYAESzQEKGE1pZ3JhdGVHaXRIdWJTb3VyY2VUb0FwcBIxLnNhYXMuYWNjb3VudHMudjEuTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVxdWVzdBoyLnNhYXMuYWNjb3VudHMudjEuTWlncmF0ZUdpdEh1YlNvdXJjZVRvQXBwUmVzcG9uc2UiSsLzGEYIAhAEKgwKBm9yZ19pZBACGAEwATomCiJzYWFzLmRhdGFzb3VyY2UuY3JlZGVudGlhbC51cGRhdGVkEAJAAUgEUAJYA2ABEtUBChpCZWdpbkRhdGFzb3VyY2VBY2NvdW50TGluaxIzLnNhYXMuYWNjb3VudHMudjEuQmVnaW5EYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0GjQuc2Fhcy5hY2NvdW50cy52MS5CZWdpbkRhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkzC8xhICAIQAyoMCgZvcmdfaWQQAhgBMAE6KAokc2Fhcy5kYXRhc291cmNlLmFjY291bnRfbGlua19zdGFydGVkEAJAAUgEUAJYBGABEtgBCh1Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGluaxI2LnNhYXMuYWNjb3VudHMudjEuQ29tcGxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5Db21wbGV0ZURhdGFzb3VyY2VBY2NvdW50TGlua1Jlc3BvbnNlIkbC8xhCCAIQAyoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmFjY291bnRfbGlua2VkEAJAAUgEUARYA2ABErUBChxMaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzEjUuc2Fhcy5hY2NvdW50cy52MS5MaXN0TXlEYXRhc291cmNlQWNjb3VudExpbmtzUmVxdWVzdBo2LnNhYXMuYWNjb3VudHMudjEuTGlzdE15RGF0YXNvdXJjZUFjY291bnRMaW5rc1Jlc3BvbnNlIibC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARLUAQobRGVsZXRlRGF0YXNvdXJjZUFjY291bnRMaW5rEjQuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5EZWxldGVEYXRhc291cmNlQWNjb3VudExpbmtSZXNwb25zZSJIwvMYRAgCEAMqDAoGb3JnX2lkEAIYATABOiQKIHNhYXMuZGF0YXNvdXJjZS5hY2NvdW50X3VubGlua2VkEAJAAUgEUAJYA2ABEqMBChZHZXREYXRhc291cmNlRGlyZWN0b3J5Ei8uc2Fhcy5hY2NvdW50cy52MS5HZXREYXRhc291cmNlRGlyZWN0b3J5UmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuR2V0RGF0YXNvdXJjZURpcmVjdG9yeVJlc3BvbnNlIibC8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ACWANgARK3AQoTQmluZERhdGFzb3VyY2VHcm91cBIsLnNhYXMuYWNjb3VudHMudjEuQmluZERhdGFzb3VyY2VHcm91cFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJDwvMYPwgCEAQqDAoGb3JnX2lkEAIYATABOh8KG3NhYXMuZGF0YXNvdXJjZS5ncm91cF9ib3VuZBACQAFIBFACWANgARK/AQoVVW5iaW5kRGF0YXNvdXJjZUdyb3VwEi4uc2Fhcy5hY2NvdW50cy52MS5VbmJpbmREYXRhc291cmNlR3JvdXBSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5VbmJpbmREYXRhc291cmNlR3JvdXBSZXNwb25zZSJFwvMYQQgCEAQqDAoGb3JnX2lkEAIYATABOiEKHXNhYXMuZGF0YXNvdXJjZS5ncm91cF91bmJvdW5kEAJAAUgEUAJYA2ABEsABChVDbGFpbURhdGFzb3VyY2VEb21haW4SLi5zYWFzLmFjY291bnRzLnYxLkNsYWltRGF0YXNvdXJjZURvbWFpblJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkNsYWltRGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkbC8xhCCAIQBCoMCgZvcmdfaWQQAhgBMAE6Igoec2Fhcy5kYXRhc291cmNlLmRvbWFpbl9jbGFpbWVkEAJAAUgEUAJYA2ABEsQBChZWZXJpZnlEYXRhc291cmNlRG9tYWluEi8uc2Fhcy5hY2NvdW50cy52MS5WZXJpZnlEYXRhc291cmNlRG9tYWluUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuVmVyaWZ5RGF0YXNvdXJjZURvbWFpblJlc3BvbnNlIkfC8xhDCAIQBCoMCgZvcmdfaWQQAhgBMAE6Iwofc2Fhcy5kYXRhc291cmNlLmRvbWFpbl92ZXJpZmllZBACQAFIBFACWANgARLDAQoWRGVsZXRlRGF0YXNvdXJjZURvbWFpbhIvLnNhYXMuYWNjb3VudHMudjEuRGVsZXRlRGF0YXNvdXJjZURvbWFpblJlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkRlbGV0ZURhdGFzb3VyY2VEb21haW5SZXNwb25zZSJGwvMYQggCEAQqDAoGb3JnX2lkEAIYATABOiIKHnNhYXMuZGF0YXNvdXJjZS5kb21haW5fcmVtb3ZlZBACQAFIBFACWANgARKgAQoVTGlzdFNvdXJjZURlbGVnYXRpb25zEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlRGVsZWdhdGlvbnNSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0U291cmNlRGVsZWdhdGlvbnNSZXNwb25zZSImwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQAlgDYAESxwEKFlJldm9rZVNvdXJjZURlbGVnYXRpb24SLy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNvdXJjZURlbGVnYXRpb25SZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTb3VyY2VEZWxlZ2F0aW9uUmVzcG9uc2UiSsLzGEYIAhAEKgwKBm9yZ19pZBACGAEwATomCiJzYWFzLmRhdGFzb3VyY2UuZGVsZWdhdGlvbi5yZXZva2VkEAJAAUgEUAJYA2ABEusBChZEZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzEi8uc2Fhcy5hY2NvdW50cy52MS5EZWxlZ2F0ZVNvdXJjZVRyaWdnZXJzUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuRGVsZWdhdGVTb3VyY2VUcmlnZ2Vyc1Jlc3BvbnNlIm7C8xhqCAIQBCoMCgZvcmdfaWQQAhgBMAE6Sgoic2Fhcy5kYXRhc291cmNlLmRlbGVnYXRpb24uY3JlYXRlZAoic2Fhcy5kYXRhc291cmNlLmRlbGVnYXRpb24ucmV2b2tlZBACQAFIBFACWANgARKyAQobR2V0U291cmNlVHJpZ2dlckRlY2xhcmF0aW9uEjQuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXF1ZXN0GjUuc2Fhcy5hY2NvdW50cy52MS5HZXRTb3VyY2VUcmlnZ2VyRGVjbGFyYXRpb25SZXNwb25zZSImwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQAlgDYAESgwMKD1JlY29uY2lsZVNvdXJjZRIoLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVzcG9uc2UimgLC8xhqCAIQAxoRZGF0YXNvdXJjZTppbnZva2UiEWRhdGFzb3VyY2U6aW52b2tlKg8KCXNvdXJjZV9pZBAEGAMwATohCh1zYWFzLmRhdGFzb3VyY2Uuc291cmNlLnN5bmNlZBACQANIBFACWAJgAdrzGKcBCgIIPBIDCNgEGAUiAggCKgtVTkFWQUlMQUJMRSoSUkVTT1VSQ0VfRVhIQVVTVEVEKgdBQk9SVEVEMg9zYWFzLWRhdGFzb3VyY2VKOS9zYWFzLmFjY291bnRzLnYxLkRhdGFzb3VyY2VTZXJ2aWNlL0xvb2t1cFJlY29uY2lsZVNvdXJjZVCAIFiAIGABchgKBnNvdXJjZRIGaW52b2tlEgRyZWFkGAESvwEKFUxvb2t1cFJlY29uY2lsZVNvdXJjZRIuLnNhYXMuYWNjb3VudHMudjEuTG9va3VwUmVjb25jaWxlU291cmNlUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmVjb25jaWxlU291cmNlUmVzcG9uc2UiS8LzGEcIAhADGg9kYXRhc291cmNlOnJlYWQiD2RhdGFzb3VyY2U6cmVhZCoPCgllZmZlY3RfaWQQBBgDMAE6AhABQAFIA1ACWAJgAUK3AQoUY29tLnNhYXMuYWNjb3VudHMudjFCD0RhdGFzb3VyY2VQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_codefly_runnable_v0_options, file_google_api_annotations, file_google_protobuf_duration, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * GitHubDatasourceConfig is the public, non-secret configuration of a GitHub @@ -2416,15 +2416,23 @@ export const RevokeSourceDelegationResponseSchema: GenMessage Date: Tue, 13 Oct 2026 17:14:45 -0400 Subject: [PATCH 10/20] fix(frontend): the schedules panel reports this host's sweep for every source, and claims nothing about who else fires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The final-amendment review of 2cda10b86 found two bounded UI gaps. Both are render-side — every fact either needs was already on the wire — and both were mine. Fixing them exposed a third of the same kind. ONE FLAG WAS ANSWERING THREE QUESTIONS. `{!declaration?.undeclarableReason && …}` wrapped all three rows, and it is true in two unrelated situations: when the source declares a usable trigger, and when there is **no declaration at all**. 1. A source that declares no trigger showed no sweep row. The sweep does not care about the declaration: its due query selects an active source whose stored instant has elapsed, which has nothing to do with whether a cron can express the interval. `DeclareSourceTrigger` computes `HostSweepDueAt` before the cron check for exactly that reason and the projection sets both before the undeclarable return — `TestAnUndeclarableSourceStillReportsTheHostSweep` has been pinning it. So an interval no five-field cron can express was shown as "Declares no trigger" and nothing else, while this host reconciled it every interval: the operator was told the host does nothing for a source it refreshes on a timer. The sweep row now needs only a declaration to have ARRIVED; the two declaration rows keep the stricter guard, since without a schedule there is no declared occurrence to show. 2. "An executing module fires the declaration" asserted that something else fires it. Nothing this host holds verifies that (#1121, F1): the switch says only that THIS host stopped, so a deployment that turned it off before a module consumed the declaration read as covered while nothing fired at all. Now: "This host does not fire it — whether an executing module does is unverified here". The holder is reported in the declared-identity row above, so the two facts are not conflated. 3. Found while fixing those: with the declaration read FAILED or in flight, `!undefined?.x` is true, so the old guard rendered the whole block — the sweep reported "off" about a host nothing had been established for, beside two em-dashed occurrence rows. Both groups now require the declaration to exist, which is the repo's own empty-versus-unknown rule. The card description no longer frames the schedule as "a trigger an executing module is meant to fire", which reads as fact beside a switched-off sweep: it says what this host declares and what this host itself does, and that whether a module consumes the declaration is not something it can confirm. EVIDENCE. Three probes written to fail first, and each does: the undeclarable source rendering no sweep row; the sweep-off cell asserting an executing module; and the failed-read case rendering rows at all. Three mutation controls, each killing a specific test — putting the sweep row back inside the undeclarable guard, restoring the old executor sentence, and lifting the row with no declaration gate at all (the naive fix, which reds four tests). The existing sweep-off test asserted the old sentence and is corrected rather than deleted, and now also asserts the old wording is absent. Feature: 12 tests (was 9). Frontend pure suite: 337/337 files, 4815/4815 tests. Docs: accounts/AGENTS.md and WORK_CONTEXTS.md say the sweep covers an undeclarable source and that a disabled sweep claims nothing about anybody else. Co-Authored-By: Claude Opus 5 (1M context) --- module/WORK_CONTEXTS.md | 10 ++ module/services/accounts/AGENTS.md | 21 +++- ...ource-trigger-schedules.container.test.tsx | 116 +++++++++++++++++- .../ui/source-trigger-schedules.tsx | 103 +++++++++++----- 4 files changed, 213 insertions(+), 37 deletions(-) diff --git a/module/WORK_CONTEXTS.md b/module/WORK_CONTEXTS.md index b457def3b..835e03e62 100644 --- a/module/WORK_CONTEXTS.md +++ b/module/WORK_CONTEXTS.md @@ -836,6 +836,16 @@ observe — hence `TestTriggerDeclarationMatchesTheContractFieldNames`, which pins the document's names *and values* and records the other column beside them. +**What the host does about a schedule is reported separately from what the +schedule says, for every source.** The stopgap sweep reconciles an active source +whose stored instant has elapsed, whether or not that source declares a trigger +— an interval no five-field cron can express is undeclarable and still swept — +so the declaration read carries `host_sweep_enabled` and `host_sweep_due_at` +beside the cron for the undeclarable case as well. And a sweep that is switched +off says only that this host stopped: whether a module consumes the declaration +is not something this host verifies, so neither the read nor the operator view +asserts it. + **A declared reference is a candidate, not a readiness claim** (#1121, F1). The host sets the reference arm for a self-addressed delegation — the only shape a source-id reference can serve — and reports whose delegation it is diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index 6de048ee2..469276329 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -1048,8 +1048,25 @@ the values and records the other column beside them). alone, and a stored instant aligned to the cron would make every source of one provider come due in the same second. `DATASOURCE_RECONCILE_SWEEP_ENABLED` (application group, ON unless `false`) is what an operator turns off when a - composed module starts consuming the declaration. The refusal is in - `RunDatasourceReconcile` itself: the one-minute tick still fires and its + composed module starts consuming the declaration. + + **Both facts are reported for a source that declares NO trigger, too**, and + the operator view shows them there: the sweep's due query selects an active + source whose stored instant has elapsed, which has nothing to do with whether + a cron could express its interval, so a source refused as undeclarable is + still reconciled every interval. `DeclareSourceTrigger` computes + `HostSweepDueAt` before the cron check for exactly that reason, and the + projection sets both before the undeclarable return + (`TestAnUndeclarableSourceStillReportsTheHostSweep`). A surface that hid the + sweep there told an operator this host does nothing for a source it refreshes + on a timer. + + **And a disabled sweep claims nothing about anybody else.** "An executing + module fires the declaration" was a readiness claim this host cannot make + (#1121, F1); the switch says only that this host stopped. The view says that + and names the gap. + + The refusal is in `RunDatasourceReconcile` itself: the one-minute tick still fires and its callback still runs, and the sweep returns having read nothing and enqueued nothing. That is deliberate — a switch read only where the ticker is installed would leave every other caller of the sweep unguarded — but it is a diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx index 4291d973b..e5601bdd4 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx @@ -259,9 +259,15 @@ describe("the host's own sweep", () => { expect( within(table).getByText(/This host’s stopgap sweep · off/), ).toBeTruthy(); + // NOT "an executing module fires the declaration", which asserted that + // something else does fire it — a readiness claim this host cannot make + // (issue #1121, F1). The switch says only that this host stopped. expect( - within(table).getByText(/An executing module fires the declaration/), + within(table).getByText(/This host does not fire it/), ).toBeTruthy(); + expect( + within(table).queryByText(/An executing module fires the declaration/), + ).toBeNull(); expect(table.querySelector('[data-slot="host-sweep-due"]')?.textContent).toBe( "—", ); @@ -272,3 +278,111 @@ describe("the host's own sweep", () => { ).toBe(new Date("2026-10-12T09:30:00Z").toLocaleString()); }); }); + +// TWO CLAIMS THE PANEL MADE THAT THIS HOST CANNOT BACK (issue #1121, the +// final-amendment review). +// +// Both are about the host-sweep row, and both are render-side: the backend +// already carries every fact either one needs. +describe("what the panel says about this host's own sweep", () => { + // The sweep does not care whether the source DECLARES a trigger. Its due + // query selects active sources whose stored instant has elapsed, and + // `DeclareSourceTrigger` computes `hostSweepDueAt` before the cron check + // precisely so this case is reportable — `TestAnUndeclarableSourceStillReportsTheHostSweep` + // pins that in pkg/adapters. So a source whose interval no five-field cron + // can express is swept every interval while the panel showed "declares no + // trigger" and nothing else: the operator was told the host does nothing + // for a source it reconciles on a timer. + it("reports the sweep for a source that declares no trigger", async () => { + serve({ + declaration: { + release: "saas_datasource_reconcile", + operation: "/saas.accounts.v1.DatasourceService/ReconcileSource", + undeclarableReason: + "the reconcile interval is not one a five-field cron expression can express", + hostSweepEnabled: true, + hostSweepDueAt: "2026-10-12T09:07:00Z", + }, + delegations: [], + children: [], + }); + renderInApp(); + await screen.findByText(/not one a five-field cron expression can express/); + const table = await waitFor(() => { + const found = document.querySelector( + '[data-slot="admissions"]', + ); + expect(found).not.toBeNull(); + return found as HTMLElement; + }); + + await waitFor(() => + expect(table.querySelector('[data-slot="host-sweep"]')).not.toBeNull(), + ); + expect(table.querySelector('[data-slot="host-sweep-due"]')?.textContent).toBe( + new Date("2026-10-12T09:07:00Z").toLocaleString(), + ); + // And the DECLARATION's own rows stay withheld, which was the half the + // old conditional had right: without a schedule there is no declared + // occurrence to show. + expect( + table.querySelectorAll('[data-slot="scheduled-occurrence"]'), + ).toHaveLength(0); + }); + + // With the sweep off, "an executing module fires the declaration" asserts + // that something else does fire it. Nothing this host holds verifies that + // (issue #1121, F1): the switch says only that THIS host stopped, so a + // deployment that turned it off before a runtime consumed the declaration + // read as covered while nothing fired at all. + it("does not claim an executing module fires it when the sweep is off", async () => { + serve({ + declaration: { + ...declared, + identityHolder: "module-a", + hostSweepEnabled: false, + hostSweepDueAt: undefined, + }, + delegations: [], + children: [], + }); + renderInApp(); + const table = await admissions(); + + const sweep = table.querySelector('[data-slot="host-sweep"]'); + expect(sweep).not.toBeNull(); + expect((sweep as HTMLElement).textContent).not.toMatch( + /An executing module fires the declaration/, + ); + expect((sweep as HTMLElement).textContent).toMatch(/unverified/); + // The holder is reported in the declared-identity row, so the two facts + // are not conflated into one claim. + expect(screen.getByText(/held by module-a/)).toBeTruthy(); + }); + + // A declaration that has not arrived — a read still in flight, or one that + // failed — must not render a sweep row at all: "off" would be an answer + // about this host that nothing established. + it("says nothing about the sweep before the declaration lands", async () => { + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + HttpResponse.json({}, { status: 500 }), + ), + ); + renderInApp(); + await screen.findByText("acme/handbook"); + + await waitFor(() => + expect( + document.querySelector('[data-slot="admissions"]'), + ).not.toBeNull(), + ); + expect(document.querySelector('[data-slot="host-sweep"]')).toBeNull(); + expect( + document.querySelector('[data-slot="scheduled-occurrence"]'), + ).toBeNull(); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx index 7167c710c..e63181ec7 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx @@ -96,14 +96,16 @@ export function SourceTriggerSchedules({ orgId }: { orgId: string }) { Reconcile schedules - Each connected source declares its reconcile schedule as a trigger an - executing module is meant to fire. Until a composed runtime consumes - that declaration, this host still fires the schedule itself, as a - stopgap — and it fires on its own stored instant, not on the declared - cron, so the two rows below are two different facts. A person's - “Sync now” admits the same operation with an effect - identity of its own, so it is listed beside the occurrences rather - than apart from them. + Each connected source declares its reconcile schedule as a trigger + for a module that executes declared work. Whether one consumes that + declaration is not something this host can confirm, so nothing below + claims it: what is shown is what this host declares, and what this + host itself does about it — the stopgap sweep, which fires on the + source's own stored instant rather than on the declared cron, so + those two rows are two different facts. A person's “Sync + now” admits the same operation with an effect identity of its + own, so it is listed beside the occurrences rather than apart from + them. @@ -206,7 +208,22 @@ function SourceTriggerRow({ - {!declaration?.undeclarableReason && ( + {/* + * THREE SEPARATE CONDITIONS, because they answer three + * questions and one flag answered them all wrongly + * (issue #1121, the final-amendment review). + * + * The declaration's own rows need a declaration that + * declares something. The host's sweep needs only a + * declaration to have ARRIVED — it sweeps a source whose + * interval no cron can express just the same. And neither + * may render before the read lands: `!declaration?.x` is + * true when `declaration` is undefined, so the old guard + * rendered the whole block over a read that had failed, + * reporting the sweep as "off" about a host nothing had + * been established for. + */} + {declaration && !declaration.undeclarableReason && ( <> @@ -232,33 +249,38 @@ function SourceTriggerRow({ — - {/* - * What will actually happen, while this host still fires the - * schedule. It is a different instant from the row above, on - * purpose: see the panel's note. With the stopgap switched off - * the host takes no pass, so the row says that rather than - * showing a time. - */} - - - {declaration?.hostSweepEnabled - ? "This host’s stopgap sweep · next pass" - : "This host’s stopgap sweep · off"} - - - {declaration?.hostSweepEnabled - ? NO_EFFECT_IDENTITY - : "An executing module fires the declaration"} - - - {declaration?.hostSweepEnabled - ? instant(declaration?.hostSweepDueAt) - : "—"} - - — - )} + {/* + * What this host will actually do, for ANY source whose + * declaration has arrived — including one that declares no + * trigger, which the sweep reconciles on its stored instant + * every interval. Hiding it there told an operator this host + * does nothing for a source it refreshes on a timer. + * + * The instant is a different one from the declared + * occurrence above, on purpose: see the panel's note. + */} + {declaration && ( + + + {declaration.hostSweepEnabled + ? "This host’s stopgap sweep · next pass" + : "This host’s stopgap sweep · off"} + + + {declaration.hostSweepEnabled + ? NO_EFFECT_IDENTITY + : SWEEP_OFF_FIRES_NOTHING_HERE} + + + {declaration.hostSweepEnabled + ? instant(declaration.hostSweepDueAt) + : "—"} + + — + + )} {passes.map((pass) => ( @@ -314,6 +336,19 @@ const MINTED_BY_THE_FIRING_MODULE = "Minted by the firing module"; */ const NO_EFFECT_IDENTITY = "None — the sweep admits no operation"; +/** + * What a switched-off sweep means, and the one thing it does not mean. + * + * It said "An executing module fires the declaration", which asserts that + * something else does — and nothing this host holds verifies that (issue + * #1121, F1). The switch says only that THIS host stopped sweeping, so a + * deployment that turned it off before a module consumed the declaration read + * as covered while nothing fired at all. The honest statement is the negative + * one plus the gap. + */ +const SWEEP_OFF_FIRES_NOTHING_HERE = + "This host does not fire it — whether an executing module does is unverified here"; + /** * One labelled fact. Exported because the playground renders the same * declaration facts in its own card, and two copies of this would be two From d2d3810d3ac2a783f77f74b9adb6989618f444e8 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:18:05 -0400 Subject: [PATCH 11/20] docs(accounts): the backfill and the declaration say self-addressed holder, never proven executor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The final amended review's advisory H2 cleanup: F1's correction reached the predicate that decides, and left the prose around it still asserting the thing F1 withdrew. Wording and one identifier only — no behaviour changes, and the same tests pass before and after. - `DeclareSourceTrigger`'s doc block said "ONLY A DELEGATION THE EXECUTING MODULE CAN ACTUALLY USE IS NAMED" and explained "what makes a delegation usable by the module that fires the trigger". It now says what is true: only a SELF-ADDRESSED delegation is named, that is a shape and not an executor, and the declaration reports whose delegation it resolved to while claiming nothing about readiness — pointing at `candidateForADeclaredExecutor`, which carries the argument. - `SourceDelegationTarget.ExecutesDeclaredWork` → `SelfAddressed`. The predicate tests one thing — the binding's audience is its own module's prefix — and the old name asserted the conclusion F1 showed does not follow. Its comment now says so outright. - the backfill's own comment, `accounts/AGENTS.md` and the `CLAIM_INVENTORY.md` row all read "self-addressed target", and each adds the limit: narrowing who may hold a CANDIDATE establishes nothing about who fires the release. - `TestDelegateSourceTriggersReplacesTheExecutorsStaleDelegation` → `...ReplacesTheSelfAddressedHoldersStaleDelegation`, with its comments following, since a test name is a claim a reader trusts. The inventory row citing it moves with it. The four remaining mentions of "executes declared work" are the honest ones — the `not claimed` row for executor readiness, and the statements that nothing in this repository composes such a module. Accounts `-tags pure ./...`: 47 packages, exit 0, zero failures, same set as before the rename. module-verify, naming gate, interface docs, authz coverage, test targets, boundaries, release-gate contract (111/0) green. Co-Authored-By: Claude Opus 5 (1M context) --- CLAIM_INVENTORY.md | 2 +- module/services/accounts/AGENTS.md | 14 +++--- .../code/pkg/business/datasource_trigger.go | 41 +++++++++-------- .../datasource_trigger_delegate_test.go | 16 ++++--- .../code/pkg/business/source_delegation.go | 45 +++++++++++-------- 5 files changed, 66 insertions(+), 52 deletions(-) diff --git a/CLAIM_INVENTORY.md b/CLAIM_INVENTORY.md index fe78c597c..a929c1bfc 100644 --- a/CLAIM_INVENTORY.md +++ b/CLAIM_INVENTORY.md @@ -98,7 +98,7 @@ must not be inferred from a row here. See | A reconcile capability is bound to the source whose DELEGATION it carries, not just to a scoped one | WORK_CONTEXTS.md § Triggers | `TestAChildFromOneSourcesDelegationIsRefusedOnAnother` (`pkg/adapters`) mints a real capability whose binding scopes two sources, presents it through `authenticateSourceOperationContext`, and asserts the delegation's own source is admitted and the other refused; red under both mutations (dropping the gate, and re-discarding the delegation) | implemented | accounts | | One occurrence's enqueue key is scoped by organization and source | accounts/AGENTS.md § A datasource source is refreshed by three things | `TestReconcileOccurrenceKeyIsScopedByOrgAndSource` (`pkg/business`): two organizations' sources under one caller-chosen effect identity produce different keys, each key names all three parts, and two attempts of one occurrence still agree | implemented | accounts | | `DelegateSourceTriggers` is additive and refuses another member's personal source | WORK_CONTEXTS.md § Triggers | `TestDelegateSourceTriggersLeavesAnotherModulesDelegationAlone`, `TestDelegateSourceTriggersRefusesAnotherMembersPersonalSource` (`pkg/business`) assert the delegation rows: another module's survives active and owner-intact, nothing is revoked, and the owner's own call records | implemented | accounts | -| The backfill records a reference only for a module that EXECUTES declared work, and replaces the executor's unusable one | accounts/AGENTS.md § Unattended work | `TestDelegateSourceTriggersLeavesAnotherModulesDelegationAlone` asserts a consuming module gets nothing; `TestDelegateSourceTriggersReplacesTheExecutorsStaleDelegation` asserts a stale-digest delegation is revoked as `binding_changed` and replaced while a current one is left untouched. Both red under the matching mutation (dropping the executor filter; always skipping an existing row) | implemented | accounts | +| The backfill records a reference only for a SELF-ADDRESSED target, and replaces that holder's unusable one | accounts/AGENTS.md § Unattended work | `TestDelegateSourceTriggersLeavesAnotherModulesDelegationAlone` asserts a target addressed to another service gets nothing; `TestDelegateSourceTriggersReplacesTheSelfAddressedHoldersStaleDelegation` asserts a stale-digest delegation is revoked as `binding_changed` and replaced while a current one is left untouched. Both red under the matching mutation (dropping the self-addressed filter; always skipping an existing row). It narrows who may hold a CANDIDATE and establishes nothing about who executes the release — see the row below | implemented | accounts | | A declared trigger's reference arm names only a SELF-ADDRESSED delegation, and reports whose it is | accounts/AGENTS.md § Unattended work; WORK_CONTEXTS.md § Triggers | `TestTriggerDeclarationNamesOnlyADelegationItsExecutorCanUse` (`pkg/business`) removes each of the three facts on its own — addressed to another module, a binding no longer declared, an authority that did not resolve — and asserts no identity is named; `TestTriggerDeclarationNamesTheHolderAndClaimsNoExecutor` varies the HOLDER independently (A-only, B-only, B revoked, B's binding changed) and pins the reported holder; `TestTriggerDeclarationProjectionCarriesEveryField` (`pkg/adapters`) holds every field to the wire | implemented | accounts | | That the module which EXECUTES a declared release holds the reference | accounts/AGENTS.md § Unattended work; WORK_CONTEXTS.md § Triggers | none, and none is possible from here (#1121, F1): nothing this host holds declares who executes declared work, so a self-addressed delegation identifies a candidate and its holder, never the executor. A composed executor with no delegation on the source is refused at the mint. The read reports the holder and leaves readiness unverified | not claimed | accounts; needs a contract naming the executing principal | | A refused reconcile pass enqueues nothing | accounts/AGENTS.md § A datasource source is refreshed by three things | `TestReconcileSourceOperation_RefusedPassEnqueuesNothing` (`pkg/business`): an effect identity re-presented for another source, and one re-presented with another request digest, are both refused with no job for them anywhere; red when the pre-check is removed | implemented | accounts | diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index 469276329..7e44c2153 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -1006,14 +1006,16 @@ the values and records the other column beside them). destructive for a backfill: it took a personal source's delegation from its owner and moved every unrelated module's authority onto the administrator, failing in-flight children's re-checks. The - backfill records **only for a module that executes declared work** — - `SourceDelegationTarget.ExecutesDeclaredWork`, the self-admitting binding - below — because that is what the reference it answers with is for (#1121). It + backfill records **only for a SELF-ADDRESSED target** — + `SourceDelegationTarget.SelfAddressed`, the self-admitting binding below — + because that is the shape the reference it answers with can serve (#1121). It recorded one for every module accepting delegations, handing the calling administrator's authority over that source to modules whose delegation no - trigger of this host could name. A connect still delegates to every target: - that is a person establishing the source, and a consuming module's sync is the - delegation's main purpose. + trigger of this host could name. It does **not** follow that such a target is + the module which will execute the release: nothing here declares that (F1), so + this narrows who may hold a candidate and proves nothing about who fires it. A + connect still delegates to every target: that is a person establishing the + source, and a consuming module's sync is the delegation's main purpose. It leaves a target's LIVE delegation untouched and records for a target with none — or with one the module's current declaration no longer supports. A diff --git a/module/services/accounts/code/pkg/business/datasource_trigger.go b/module/services/accounts/code/pkg/business/datasource_trigger.go index 7bd8f5413..9726dae33 100644 --- a/module/services/accounts/code/pkg/business/datasource_trigger.go +++ b/module/services/accounts/code/pkg/business/datasource_trigger.go @@ -416,27 +416,30 @@ func datasourceTriggerInput(orgID, sourceID string) []byte { // stated as planned in MODULE.md and WORK_CONTEXTS.md rather than quietly // shortened to fit. // -// ONLY A DELEGATION THE EXECUTING MODULE CAN ACTUALLY USE IS NAMED (issue -// #1121, m5). It used to take the first active reference from any module, so a -// deployment whose executing module holds none read as "bound" because some -// other module's delegation existed — and every occurrence was then refused at -// the mint, with the page saying the opposite. +// ONLY A SELF-ADDRESSED DELEGATION IS NAMED, AND IT IS A CANDIDATE (issue +// #1121, m5 and F1). It used to take the first active reference from any +// module, which named delegations no trigger could ever use: a mint by source +// id resolves the active delegation OF THE CALLING MODULE +// (ActiveSourceDelegation by source and prefix) and the capability is addressed +// to the binding's declared audience, which the module admitting it must itself +// accept. Both are true of exactly one shape — a delegation whose binding names +// its own module as the audience, the self-admitting binding +// selfAdmittingBinding exists to allow. A delegation to module A addressed to +// audience B is reachable only through the delegation-id exchange, which a +// source-id reference cannot name. // -// What makes a delegation usable by the module that fires the trigger is -// derivable from the delegation itself, and needs no new configuration: the -// reference arm is a source id, a mint by source id resolves the active -// delegation OF THE CALLING MODULE (ActiveSourceDelegation by source and -// prefix), and the capability it answers with is addressed to the binding's -// declared audience — which the executing module must itself admit, because it -// attaches the capability as its own incoming execution context. Both are true -// of exactly one shape: a delegation whose binding names its own module as the -// audience, which is the self-admitting binding selfAdmittingBinding exists to -// allow. A delegation to module A addressed to audience B is usable only -// through the delegation-id exchange, which a source-id reference cannot reach. +// THAT IS A SHAPE, NOT AN EXECUTOR. Nothing this host holds declares which +// module executes declared work — self-addressed bindings are generic and a +// deployment may have several — so a source whose only such delegation belongs +// to module A is a candidate under A's name, and if the composed executor is B, +// holding none, B's mint is refused with ErrSourceDelegationMissing. The +// declaration therefore reports WHOSE delegation it resolved to +// (SourceTriggerDeclaration.IdentityHolder) and claims nothing about readiness; +// see candidateForADeclaredExecutor, which carries the whole argument. // -// An active delegation with no resolvable scopes is not one either: the module -// no longer declares the binding it was recorded against, so the next mint -// revokes it as binding_changed. +// An active delegation with no resolvable scopes is not a candidate either: the +// module no longer declares the binding it was recorded against, so the next +// mint revokes it as binding_changed. // // THE REFERENCE IS THE SOURCE ID, AND REVOCATION IS THEREFORE NOT DURABLE // (issue #1112, M3). Every delegation on one source reads back under the same diff --git a/module/services/accounts/code/pkg/business/datasource_trigger_delegate_test.go b/module/services/accounts/code/pkg/business/datasource_trigger_delegate_test.go index 8136d0772..0e0c87c3c 100644 --- a/module/services/accounts/code/pkg/business/datasource_trigger_delegate_test.go +++ b/module/services/accounts/code/pkg/business/datasource_trigger_delegate_test.go @@ -105,17 +105,19 @@ func TestDelegateSourceTriggersLeavesAnotherModulesDelegationAlone(t *testing.T) require.Equal(t, 0, store.revocations, "a backfill revoked something") } -// A delegation the executor's own current declaration no longer supports is -// not "nothing was missing" (issue #1121, N8). +// A delegation the HOLDER's own current declaration no longer supports is not +// "nothing was missing" (issue #1121, N8). Holder, not executor: a +// self-addressed target is one that COULD serve a trigger, never one proved to +// fire the release (F1). // // A delegation carries a digest of its binding's audience and delegation // scopes, and the very next mint revokes one whose binding has changed // (`binding_changed`). The backfill used to skip any target holding an active -// row, so an operator pressing the button over a source whose executor's -// binding had moved was told nothing was missing — over a trigger that could -// never mint. It is revoked here, under the same reason the mint would use, and +// row, so an operator pressing the button over a source whose self-addressed +// holder's binding had moved was told nothing was missing — over a trigger that +// could never mint. It is revoked here, under the same reason the mint would use, and // a fresh one is recorded in the same transaction. -func TestDelegateSourceTriggersReplacesTheExecutorsStaleDelegation(t *testing.T) { +func TestDelegateSourceTriggersReplacesTheSelfAddressedHoldersStaleDelegation(t *testing.T) { store := newDelegateTriggerStore(t) svc, _ := newDatasourceService(store, &recordingProducer{}, &fakeGitHub{defaultBranch: "main", commit: "abc"}) source := githubSource(t, svc, "main", nil, "") @@ -123,7 +125,7 @@ func TestDelegateSourceTriggersReplacesTheExecutorsStaleDelegation(t *testing.T) require.NoError(t, err) svc.SetModuleCapabilities(nil, nil, registry) - // The executor's delegation as recorded against an EARLIER shape of its + // The holder's delegation as recorded against an EARLIER shape of its // binding: same binding id, a digest that no longer matches. stale := store.insert(source.ID, "connecting-member", "runtime", registry) stale.BindingDigest = "0" + stale.BindingDigest[1:] diff --git a/module/services/accounts/code/pkg/business/source_delegation.go b/module/services/accounts/code/pkg/business/source_delegation.go index 110629137..fed37a228 100644 --- a/module/services/accounts/code/pkg/business/source_delegation.go +++ b/module/services/accounts/code/pkg/business/source_delegation.go @@ -229,19 +229,24 @@ type SourceDelegationTarget struct { Binding ModuleOperationAudience } -// ExecutesDeclaredWork reports whether this target's binding is one the module -// holding it can admit as its OWN incoming execution context: its audience is -// its own declared prefix. +// SelfAddressed reports whether this target's binding is one the module holding +// it can admit as its OWN incoming execution context: its audience is its own +// declared prefix. // -// It is what makes a target the executing module of a declared trigger rather -// than a module whose delegation some other module presents (issue #1121, N8). -// A trigger's reference arm is a source id, a mint by source id resolves the -// active delegation of the CALLING module, and the capability is addressed to -// the binding's audience — so a delegation to module A addressed to audience B -// can serve a trigger only through the delegation-id exchange, which no -// reference this host declares can reach. See selfAdmittingBinding for why -// naming your own prefix is admitted at all. -func (t SourceDelegationTarget) ExecutesDeclaredWork() bool { +// It is the shape a declared trigger's reference arm can serve (issue #1121, +// N8), and nothing more than that: a trigger's reference is a source id, a mint +// by source id resolves the active delegation of the CALLING module, and the +// capability is addressed to the binding's audience — so a delegation to module +// A addressed to audience B can serve a trigger only through the delegation-id +// exchange, which no reference this host declares can reach. See +// selfAdmittingBinding for why naming your own prefix is admitted at all. +// +// IT DOES NOT IDENTIFY THE EXECUTOR (issue #1121, F1). Nothing this host holds +// declares which module executes declared work, and these bindings are generic, +// so a self-addressed target is a candidate the backfill may record for — never +// a module proved to install and fire the release. The name says "self +// addressed" because that is the fact it tests. +func (t SourceDelegationTarget) SelfAddressed() bool { return t.Binding.Audience != "" && t.Binding.Audience == t.Prefix } @@ -287,15 +292,17 @@ const ( // delegation exactly as it is, and only a target with none — or with one // its module's current declaration no longer supports — gets one. // - // It is also narrowed to the modules that EXECUTE declared work - // (SourceDelegationTarget.ExecutesDeclaredWork), because that is what it is - // for: the reference a declared release's trigger names. It used to record + // It is also narrowed to SELF-ADDRESSED targets + // (SourceDelegationTarget.SelfAddressed), because that is the shape the + // reference a declared release's trigger names can serve. It used to record // one for every module accepting delegations, which hands the calling // administrator's authority over that source to modules whose delegation no // trigger of this host could ever name — authority nobody asked to grant - // (issue #1121, N8). A connect still delegates to every target: that is a - // person establishing the source, and the consuming module's sync is the - // delegation's main purpose. + // (issue #1121, N8). It does not follow that a self-addressed target is the + // module which will execute the release; nothing here declares that (F1). + // A connect still delegates to every target: that is a person establishing + // the source, and the consuming module's sync is the delegation's main + // purpose. // // The distinction is issue #1112, M2. The backfill used to run the replace // mode, which took every delegation on the source, marked it `replaced` and @@ -338,7 +345,7 @@ func (s *Service) recordSourceDelegationsTx( recorded := 0 for _, target := range targets { if mode == sourceDelegationBackfill { - if !target.ExecutesDeclaredWork() { + if !target.SelfAddressed() { continue } existing, err := s.store.ActiveSourceDelegation(ctx, sourceID, target.Prefix) From b3d01c3d3b453a202e2f9b2a04a9a4bdd0498c06 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:25:05 -0400 Subject: [PATCH 12/20] fix(frontend): one source's row classifies its own declaration read, and says "nothing due" rather than a pass with no time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The final UI review found three display defects, all in `SourceTriggerRow` and all mine. The panel classified the source LIST read; the per-source declaration read was never classified at all, which is where the first two come from. 1. A REFUSED OR PENDING READ REPORTED AN ABSENT IDENTITY. The row read only `read.data`, so a 403, a read still in flight and a trigger that genuinely has no identity all rendered the same sentence — "None — occurrences would be refused". That is a statement about this source's authority the host never made: a viewer whose read was denied was told the trigger has no identity rather than that they cannot see it, and `declaredIdentity(undefined)` treated unknown as absent. The row now classifies its own read (`readOutcome`), renders the kit's never-flashing wait while it is in flight, and otherwise says which of the three it is. The declaration's facts render only when a declaration exists — a dash in `Release` would read as "this host has none". 2. SUCCESS → 403 KEPT THE CACHED DECLARATION, including the sweep row this amendment added. TanStack keeps the last successful answer when a refetch rejects, so authority withdrawn mid-session left the previous answer on screen. The clearing goes where the refusal is classified — in the query function, over every cached key of what it was reading — because authority is a property of the read, not of whichever component is mounted. Only a refusal clears; a transient failure keeps the prior answer under a stale notice, and the manual rows sit outside every declaration branch because a person's pass goes through no schedule at all. 3. SWEEP ON WITH NO DUE INSTANT SAID "next pass" AND A DASH. A parked source's stored instant is cleared by the park path, so its absence is a fact — the source is not on the sweep's schedule — and not a missing time. Three states now, because three are supplied: a pass due, on with nothing due for this source, and off. EVIDENCE. Three probes, each confirmed failing first (3 failed | 8 passed), then passing. Three mutation controls, each killing its own probe: reverting the query-side clearing to its 120a6c7b1 form (1 red); the no-due label back to "next pass" (1 red); and rendering the facts without the declaration gate (2 red — the cold refusal and the retained snapshot). Feature: 15 tests (was 12). Frontend pure in full, serial: 337/337 files, 4818/4818 tests, exit 0. Typecheck, biome, module-verify, naming gate, interface docs, test targets, story tests, boundaries, kit-version, ui-inventory green. No backend change, so the accounts suites are not re-run and not claimed. Co-Authored-By: Claude Opus 5 (1M context) --- .../unattended-work/service/queries.ts | 30 +++- ...ource-trigger-schedules.container.test.tsx | 156 ++++++++++++++++-- .../ui/source-trigger-schedules.tsx | 103 +++++++++--- 3 files changed, 250 insertions(+), 39 deletions(-) diff --git a/module/services/frontend/code/src/features/unattended-work/service/queries.ts b/module/services/frontend/code/src/features/unattended-work/service/queries.ts index e42ebac7a..31378617c 100644 --- a/module/services/frontend/code/src/features/unattended-work/service/queries.ts +++ b/module/services/frontend/code/src/features/unattended-work/service/queries.ts @@ -1,5 +1,6 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useDatasourceService } from "@/lib/hooks/use-api-client"; +import { mayKeepRetainedRows, readOutcome } from "@/shared/lib/read-outcome"; /** * The reads behind the unattended-work playground. @@ -32,9 +33,34 @@ export function useTriggerSources(orgId: string) { */ export function useSourceTriggerDeclaration(orgId: string, sourceId: string) { const service = useDatasourceService(); + const client = useQueryClient(); return useQuery({ queryKey: unattendedWorkKeys.declaration(orgId, sourceId), enabled: !!orgId && !!sourceId, - queryFn: () => service.getSourceTriggerDeclaration({ orgId, sourceId }), + queryFn: async () => { + try { + return await service.getSourceTriggerDeclaration({ orgId, sourceId }); + } catch (error) { + // A REFUSAL DELETES WHAT IT WAS READING, and it does it here + // rather than at render (issue #1121). TanStack keeps the last + // successful answer when a refetch rejects, so a viewer whose + // authority was withdrawn went on seeing the declaration — and + // the sweep row beside it — from cache. Authority is a property + // of the read, so what the read may leave behind is too. + // + // Only a refusal clears. A transient failure keeps the prior + // answer under a stale notice, because blanking a good answer + // because one request did not come back is worse than the + // staleness (shared/lib/read-outcome.ts). + if (!mayKeepRetainedRows(readOutcome(true, error))) { + for (const query of client + .getQueryCache() + .findAll({ queryKey: ["source-trigger-declaration"] })) { + query.setState({ data: undefined, dataUpdatedAt: 0 }); + } + } + throw error; + } + }, }); } diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx index e5601bdd4..050e2688b 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx @@ -234,9 +234,9 @@ describe("the host's own sweep", () => { expect( table.querySelector('[data-slot="next-occurrence"]')?.textContent, ).toBe(declaredAt); - expect(table.querySelector('[data-slot="host-sweep-due"]')?.textContent).toBe( - sweepAt, - ); + expect( + table.querySelector('[data-slot="host-sweep-due"]')?.textContent, + ).toBe(sweepAt); // The sweep admits no operation, so it has no effect identity to show — // said rather than left as a dash that would read as "it has none". expect( @@ -249,7 +249,11 @@ describe("the host's own sweep", () => { // never both run. With it off there is no host pass at all, and showing // an instant would be describing one. serve({ - declaration: { ...declared, hostSweepEnabled: false, hostSweepDueAt: undefined }, + declaration: { + ...declared, + hostSweepEnabled: false, + hostSweepDueAt: undefined, + }, delegations: [], children: [], }); @@ -262,15 +266,13 @@ describe("the host's own sweep", () => { // NOT "an executing module fires the declaration", which asserted that // something else does fire it — a readiness claim this host cannot make // (issue #1121, F1). The switch says only that this host stopped. - expect( - within(table).getByText(/This host does not fire it/), - ).toBeTruthy(); + expect(within(table).getByText(/This host does not fire it/)).toBeTruthy(); expect( within(table).queryByText(/An executing module fires the declaration/), ).toBeNull(); - expect(table.querySelector('[data-slot="host-sweep-due"]')?.textContent).toBe( - "—", - ); + expect( + table.querySelector('[data-slot="host-sweep-due"]')?.textContent, + ).toBe("—"); // The declaration itself is unchanged by the switch: it is what the // source declares, not what this host does about it. expect( @@ -319,9 +321,9 @@ describe("what the panel says about this host's own sweep", () => { await waitFor(() => expect(table.querySelector('[data-slot="host-sweep"]')).not.toBeNull(), ); - expect(table.querySelector('[data-slot="host-sweep-due"]')?.textContent).toBe( - new Date("2026-10-12T09:07:00Z").toLocaleString(), - ); + expect( + table.querySelector('[data-slot="host-sweep-due"]')?.textContent, + ).toBe(new Date("2026-10-12T09:07:00Z").toLocaleString()); // And the DECLARATION's own rows stay withheld, which was the half the // old conditional had right: without a schedule there is no declared // occurrence to show. @@ -375,14 +377,138 @@ describe("what the panel says about this host's own sweep", () => { renderInApp(); await screen.findByText("acme/handbook"); + await waitFor(() => + expect(document.querySelector('[data-slot="admissions"]')).not.toBeNull(), + ); + expect(document.querySelector('[data-slot="host-sweep"]')).toBeNull(); + expect( + document.querySelector('[data-slot="scheduled-occurrence"]'), + ).toBeNull(); + }); +}); + +// WHAT ONE SOURCE'S ROW SAYS WHEN THE DECLARATION READ DID NOT SUCCEED +// (issue #1121, the final UI review). +// +// The list read is classified; the PER-SOURCE declaration read was not, and a +// row that reads only `data` answers three different situations the same way. +// All three probes drive the real hooks through the container harness. +describe("a per-source declaration read that is refused, pending or stale", () => { + // Cold refusal: the host answered nothing, and the row said "None — + // occurrences would be refused", which is a definite statement about this + // source's identity. `declaredIdentity(undefined)` treated unknown as + // absent, so a viewer whose read was denied was told the trigger has no + // identity rather than that they cannot see it. + it("does not report an absent identity when the read was refused", async () => { + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + HttpResponse.json( + { code: "permission_denied", message: "denied" }, + { status: 403 }, + ), + ), + ); + renderInApp(); + await screen.findByText("acme/handbook"); + await waitFor(() => expect( - document.querySelector('[data-slot="admissions"]'), + document.querySelector('[data-slot="source-trigger"]'), ).not.toBeNull(), ); + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); + expect( + screen.queryByText("None — occurrences would be refused"), + ).toBeNull(); + // And nothing about this host's own sweep, which the read never answered. expect(document.querySelector('[data-slot="host-sweep"]')).toBeNull(); + }); + + // A refusal AFTER a success must not leave the previous answer on screen: + // TanStack keeps the last good data when a refetch rejects, so the row went + // on showing a cached declaration — including the sweep row this amendment + // added — to a viewer who is no longer allowed to see it. The clearing + // belongs in the query function, where the refusal is classified. + it("drops a cached declaration when a later read is refused", async () => { + let denied = false; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + denied + ? HttpResponse.json( + { code: "permission_denied", message: "denied" }, + { status: 403 }, + ) + : HttpResponse.json({ + declaration: { + release: "saas_datasource_reconcile", + operation: + "/saas.accounts.v1.DatasourceService/ReconcileSource", + undeclarableReason: "the source has no reconcile schedule", + hostSweepEnabled: true, + hostSweepDueAt: "2026-10-12T09:07:00Z", + }, + delegations: [], + children: [], + }), + ), + ); + const { client } = renderInApp(); + await screen.findByText("the source has no reconcile schedule"); + await waitFor(() => + expect(document.querySelector('[data-slot="host-sweep"]')).not.toBeNull(), + ); + + denied = true; + await act(async () => { + await client.refetchQueries({ queryKey: ["source-trigger-declaration"] }); + }); + + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); expect( - document.querySelector('[data-slot="scheduled-occurrence"]'), + screen.queryByText("the source has no reconcile schedule"), ).toBeNull(); + expect(document.querySelector('[data-slot="host-sweep"]')).toBeNull(); + }); + + // The sweep is ON and this source has no due instant — a parked source, + // whose stored instant the park path cleared. "next pass —" reads as a pass + // whose time is unknown; the supplied facts say there is no pass due. + it("says the sweep has nothing due rather than a next pass with no time", async () => { + serve({ + declaration: { + release: "saas_datasource_reconcile", + operation: "/saas.accounts.v1.DatasourceService/ReconcileSource", + undeclarableReason: + "the source is parked and is not on its reconcile schedule", + hostSweepEnabled: true, + }, + delegations: [], + children: [], + }); + renderInApp(); + await screen.findByText(/parked and is not on its reconcile schedule/); + const sweep = await waitFor(() => { + const found = document.querySelector( + '[data-slot="host-sweep"]', + ); + expect(found).not.toBeNull(); + return found as HTMLElement; + }); + + expect(sweep.textContent).not.toMatch(/next pass/); + expect(sweep.textContent).toMatch(/nothing due/i); + expect( + sweep.querySelector('[data-slot="host-sweep-due"]')?.textContent, + ).toBe("—"); }); }); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx index e63181ec7..8cb09a223 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx @@ -28,6 +28,7 @@ import { TableHead, TableHeader, TableRow, + useLoadingPhase, } from "@/shared/ui"; import { sourceLabel } from "../model/source-label"; import { @@ -96,8 +97,8 @@ export function SourceTriggerSchedules({ orgId }: { orgId: string }) { Reconcile schedules - Each connected source declares its reconcile schedule as a trigger - for a module that executes declared work. Whether one consumes that + Each connected source declares its reconcile schedule as a trigger for + a module that executes declared work. Whether one consumes that declaration is not something this host can confirm, so nothing below claims it: what is shown is what this host declares, and what this host itself does about it — the stopgap sweep, which fires on the @@ -166,7 +167,18 @@ function SourceTriggerRow({ passes: AdmittedReconcilePass[]; }) { const read = useSourceTriggerDeclaration(orgId, sourceId); + // THIS ROW'S OWN READ IS CLASSIFIED, not just its data (issue #1121). The + // source LIST is classified by the panel above; the per-source declaration + // was not, so a refused read, a read still in flight and a source whose + // trigger genuinely has no identity all rendered the same sentence — "None + // — occurrences would be refused", which is a statement about this source's + // authority that the host never made. A viewer denied the read was told the + // trigger has no identity rather than that they cannot see it. + const outcome = readOutcome(read.isError, read.error); const declaration = read.data?.declaration; + const stale = declaration ? staleReadNotice(outcome, "this trigger") : null; + const phase = useLoadingPhase(read.isFetching); + const waiting = phase.indicator || phase.quiet; const identity = useMemo(() => declaredIdentity(declaration), [declaration]); // The operation is read off the declaration so the manual rows name the // same string the scheduled ones do. A literal here would be this page's @@ -177,22 +189,50 @@ function SourceTriggerRow({ // A source that declares no trigger still takes a manual pass: the // declaration is refused for the interval it carries, not for the // operation, and a person's pass does not go through a schedule at all. So - // the undeclarable reason replaces the SCHEDULE's rows, never the table. + // the undeclarable reason replaces the SCHEDULE's rows, never the table — + // and neither does a read that did not answer, which is why the manual rows + // below sit outside every declaration branch. return (
{label} - {declaration?.undeclarableReason ? ( - Declares no trigger - ) : ( - {declaration?.schedule?.cron ?? "—"} - )} + {declaration ? ( + declaration.undeclarableReason ? ( + Declares no trigger + ) : ( + {declaration.schedule?.cron ?? "—"} + ) + ) : null}
-
- - - -
+ + {stale && ( +

+ {stale} +

+ )} + {/* + * The declaration's facts need a declaration. Without one the read + * either has not answered or was refused, and the message says + * which — never a dash, which would read as "this host has none". + */} + {declaration ? ( +
+ + + +
+ ) : waiting ? null : ( +

+ {readOutcomeMessage( + outcome, + "this source's declared trigger", + "This source declares no trigger.", + )} +

+ )} {declaration?.undeclarableReason && (

{declaration.undeclarableReason} @@ -264,19 +304,29 @@ function SourceTriggerRow({ {declaration && ( - {declaration.hostSweepEnabled - ? "This host’s stopgap sweep · next pass" - : "This host’s stopgap sweep · off"} + {/* + * Three states, because the facts supplied are three + * (issue #1121). Enabled WITH an instant is a pass + * due. Enabled with none is a source the sweep will + * not reach — a parked source, whose stored instant + * the park path cleared — and "next pass —" read as a + * pass whose time was unknown. Disabled is neither. + */} + {!declaration.hostSweepEnabled + ? "This host’s stopgap sweep · off" + : declaration.hostSweepDueAt + ? "This host’s stopgap sweep · next pass" + : "This host’s stopgap sweep · on, nothing due for this source"} - {declaration.hostSweepEnabled - ? NO_EFFECT_IDENTITY - : SWEEP_OFF_FIRES_NOTHING_HERE} + {!declaration.hostSweepEnabled + ? SWEEP_OFF_FIRES_NOTHING_HERE + : declaration.hostSweepDueAt + ? NO_EFFECT_IDENTITY + : SWEEP_ON_NOTHING_DUE} - {declaration.hostSweepEnabled - ? instant(declaration.hostSweepDueAt) - : "—"} + {instant(declaration.hostSweepDueAt)} — @@ -346,6 +396,15 @@ const NO_EFFECT_IDENTITY = "None — the sweep admits no operation"; * as covered while nothing fired at all. The honest statement is the negative * one plus the gap. */ +/** + * The sweep is running and this source has no instant in it. Said rather than + * shown as a missing time: the stored instant is what the park and restore + * paths clear, so its absence is the source being off the schedule — a fact, + * not a gap in what the host answered. + */ +const SWEEP_ON_NOTHING_DUE = + "None — this source is not on the sweep's schedule"; + const SWEEP_OFF_FIRES_NOTHING_HERE = "This host does not fire it — whether an executing module does is unverified here"; From 6d331a17c3f6a92da9e71a695ee1ea1182a75d7e Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:31:25 -0400 Subject: [PATCH 13/20] test(frontend): close every declaration read state, and say so in the playground card too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit r22's F1 closure list named six conditions and an affected consumer. b3d01c3d3 fixed the mechanism; this proves the whole list and fixes the one consumer that still dashed its facts. WHAT WAS ALREADY CLOSED BY b3d01c3d3, now each with its own assertion: a read in flight claims nothing (a declaration held open by the test, so "pending" is a state and not a race); a non-refusal failure reads as unknown rather than forbidden; 403 after success drops the cached answer; a transient failure keeps the prior answer AND labels it stale; manual admissions survive a refusal, because they come from the kit's notification rather than the read. TWO CONDITIONS THAT HAD NO TEST, and the one that would have caught a real regression: a refused snapshot must not come back through a LATER TRANSIENT FAILURE, nor through a REMOUNT into the same cache. Both hold — the snapshot is deleted at the query boundary, so there is nothing for a later failure to retain — and both go red when the clearing is reverted. Recovery is also asserted: after a refusal, one successful read restores the declaration and the sweep row. THE PLAYGROUND IS THE OTHER CONSUMER of that shared query, as the review said. It already withheld refused rows, which was the half that was right, and then rendered every fact as an em dash — which reads as "this host declares nothing" to a viewer who is simply not allowed to see it. Its declaration card now says which state the read is in, keeps a transiently failed answer under a stale notice, and carries `data-slot="declared-trigger"` so an assertion can be scoped to it. That slot mattered: my first probe passed against the UNFIXED card, because the delegations card below carries the same sentence about its own read and an unscoped match found it. Scoped, both probes fail on b3d01c3d3's card and pass on this one. Mutation controls: reverting the query-side clearing to its 120a6c7b1 form reds 4 of the schedule tests (success→403, 403→transient, 403→remount, manual admissions); reverting the playground card to b3d01c3d3 reds both of its probes. Counts: unattended-work feature 15 → 24 tests (schedules 18, playground 6). Frontend pure in full, serial: 337/337 files, 4827/4827, exit 0. Typecheck, biome, module-verify, naming gate, interface docs, test targets, story tests, boundaries, kit-version, ui-inventory green. No backend change. One thing deliberately not changed: `readOutcomeMessage`'s second clause reads "There may be some" for a singular subject like a declaration. It is the shared helper every admin surface uses and the review asked for that mechanism, so a wording change there belongs in its own change rather than inside a frozen review window. Co-Authored-By: Claude Opus 5 (1M context) --- ...ource-trigger-schedules.container.test.tsx | 269 ++++++++++++++++++ ...ttended-work-playground.container.test.tsx | 91 +++++- .../ui/unattended-work-playground.tsx | 35 ++- 3 files changed, 392 insertions(+), 3 deletions(-) diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx index 050e2688b..7e947f454 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx @@ -512,3 +512,272 @@ describe("a per-source declaration read that is refused, pending or stale", () = ).toBe("—"); }); }); + +// THE REST OF THE CLOSURE CONDITIONS for the per-source declaration read +// (issue #1121, r22 F1): pending, a non-refusal failure, 403 then a transient +// failure, 403 then a remount, recovery on success, and manual admissions +// surviving all of it. +// +// A declaration served only after the test releases it, so "pending" is a real +// state rather than a race. +function heldDeclaration() { + let release: (() => void) | undefined; + const gate = new Promise((resolve) => { + release = resolve; + }); + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post( + rpc("DatasourceService", "GetSourceTriggerDeclaration"), + async () => { + await gate; + return HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }); + }, + ), + ); + return () => release?.(); +} + +describe("the declaration read's remaining states", () => { + it("claims nothing while the read is still in flight", async () => { + const release = heldDeclaration(); + renderInApp(); + await screen.findByText("acme/handbook"); + + // The row exists; its declaration does not yet. Neither an absent + // identity nor a sweep state may be asserted about a read in flight. + await waitFor(() => + expect( + document.querySelector('[data-slot="source-trigger"]'), + ).not.toBeNull(), + ); + expect( + screen.queryByText("None — occurrences would be refused"), + ).toBeNull(); + expect(document.querySelector('[data-slot="host-sweep"]')).toBeNull(); + + release(); + await screen.findByText("*/30 * * * *"); + }); + + // A failure that is NOT a refusal reads differently, because the two mean + // different things: unknown versus not allowed. + it("separates a failed read from a refused one", async () => { + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + HttpResponse.json({ message: "boom" }, { status: 500 }), + ), + ); + renderInApp(); + await screen.findByText("acme/handbook"); + + await waitFor(() => + expect(screen.getByText(/Couldn.t load/i)).toBeTruthy(), + ); + expect(screen.queryByText(/don.t have permission/i)).toBeNull(); + expect( + screen.queryByText("None — occurrences would be refused"), + ).toBeNull(); + }); + + // A refusal, then a transient failure. The snapshot the refusal deleted must + // not come back: a transient failure retains what the cache HAS, and after a + // refusal it has nothing. + it("does not resurrect a refused snapshot through a later transient failure", async () => { + let phase: "ok" | "denied" | "down" = "ok"; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => { + if (phase === "ok") { + return HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }); + } + return phase === "denied" + ? HttpResponse.json({ code: "permission_denied" }, { status: 403 }) + : HttpResponse.json({ message: "boom" }, { status: 500 }); + }), + ); + const { client } = renderInApp(); + await screen.findByText("*/30 * * * *"); + + phase = "denied"; + await act(async () => { + await client.refetchQueries({ queryKey: ["source-trigger-declaration"] }); + }); + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); + + phase = "down"; + await act(async () => { + await client.refetchQueries({ queryKey: ["source-trigger-declaration"] }); + }); + expect(screen.queryByText("*/30 * * * *")).toBeNull(); + expect(document.querySelector('[data-slot="host-sweep"]')).toBeNull(); + }); + + // And not through a remount either: the snapshot is gone from the cache, so + // a component mounting into that same cache has nothing to show. + it("does not resurrect a refused snapshot through a remount", async () => { + let denied = false; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + denied + ? HttpResponse.json({ code: "permission_denied" }, { status: 403 }) + : HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }), + ), + ); + const first = renderInApp(); + await screen.findByText("*/30 * * * *"); + + denied = true; + await act(async () => { + await first.client.refetchQueries({ + queryKey: ["source-trigger-declaration"], + }); + }); + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); + + cleanup(); + renderInApp(, first.client); + await screen.findByText("acme/handbook"); + expect(screen.queryByText("*/30 * * * *")).toBeNull(); + expect(document.querySelector('[data-slot="host-sweep"]')).toBeNull(); + }); + + // Recovery is exactly one thing: a read that succeeds. + it("recovers when a later read succeeds", async () => { + let denied = true; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + denied + ? HttpResponse.json({ code: "permission_denied" }, { status: 403 }) + : HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }), + ), + ); + const { client } = renderInApp(); + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); + + denied = false; + await act(async () => { + await client.refetchQueries({ queryKey: ["source-trigger-declaration"] }); + }); + await screen.findByText("*/30 * * * *"); + await waitFor(() => + expect(document.querySelector('[data-slot="host-sweep"]')).not.toBeNull(), + ); + expect(screen.queryByText(/don.t have permission/i)).toBeNull(); + }); + + // A transient failure keeps the last good answer AND says it may be out of + // date. Blanking a good answer because one request did not come back is + // worse than the staleness. + it("keeps a transiently failed answer and labels it stale", async () => { + let down = false; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + down + ? HttpResponse.json({ message: "boom" }, { status: 500 }) + : HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }), + ), + ); + const { client } = renderInApp(); + await screen.findByText("*/30 * * * *"); + + down = true; + await act(async () => { + await client.refetchQueries({ queryKey: ["source-trigger-declaration"] }); + }); + + expect(screen.getByText("*/30 * * * *")).toBeTruthy(); + await waitFor(() => + expect(screen.getByText(/may be out of date/i)).toBeTruthy(), + ); + }); + + // Through every one of those, the passes THIS BROWSER admitted stay: they + // come from the kit's notification, not from the declaration read, and a + // person's pass goes through no schedule at all. + it("keeps manual admissions through a refusal", async () => { + let denied = false; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + denied + ? HttpResponse.json({ code: "permission_denied" }, { status: 403 }) + : HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }), + ), + ); + const { client } = renderInApp(); + await screen.findByText("*/30 * * * *"); + act(() => + notifySourceReconcileAdmitted({ + sourceId: SOURCE, + effectId: "effect-dddd", + jobId: "job-9", + status: "committed", + repeat: false, + at: "2026-10-12T09:20:00Z", + }), + ); + await screen.findByText("effect-dddd"); + + denied = true; + await act(async () => { + await client.refetchQueries({ queryKey: ["source-trigger-declaration"] }); + }); + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); + + expect(screen.getByText("effect-dddd")).toBeTruthy(); + expect(document.querySelectorAll('[data-slot="manual-pass"]')).toHaveLength( + 1, + ); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx index 545ed77d3..409f02f0e 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx @@ -1,4 +1,11 @@ -import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react"; +import { + act, + cleanup, + fireEvent, + screen, + waitFor, + within, +} from "@testing-library/react"; import { HttpResponse, http, type JsonBodyType } from "msw"; import { toast } from "sonner"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -252,3 +259,85 @@ describe("UnattendedWorkPlayground", () => { ).toBeTruthy(); }); }); + +// THE PLAYGROUND IS THE OTHER CONSUMER OF THE SHARED DECLARATION QUERY +// (issue #1121, r22 F1 closure). It already withheld retained rows on refusal, +// which is the half that was right; what it did not do is SAY so — the facts +// rendered as em dashes, which read as "this host declares nothing" to a viewer +// who is simply not allowed to see it. The fix to the shared query deletes the +// snapshot, so this card must now explain the state rather than dash it. +describe("the playground's declaration card when the read does not succeed", () => { + function serveDeclaration(handler: () => Response) { + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + handler(), + ), + ); + } + + it("says a refused declaration was refused, rather than dashing its facts", async () => { + serveDeclaration(() => + HttpResponse.json({ code: "permission_denied" }, { status: 403 }), + ); + renderInApp(); + await screen.findByText("acme/handbook"); + + // Scoped to the DECLARATION card: the delegations card below it carries + // its own refusal message, so a page-wide match would pass without this + // card saying anything at all. + const card = await waitFor(() => { + const found = document.querySelector( + '[data-slot="declared-trigger"]', + ); + expect(found).not.toBeNull(); + return found as HTMLElement; + }); + await waitFor(() => + expect(within(card).getByText(/don.t have permission/i)).toBeTruthy(), + ); + // No invented facts: the release and operation are the host's answer, + // and it gave none. + expect(screen.queryByText("saas_datasource_reconcile")).toBeNull(); + }); + + it("recovers the declaration card when a later read succeeds", async () => { + let denied = true; + serveDeclaration(() => + denied + ? HttpResponse.json({ code: "permission_denied" }, { status: 403 }) + : HttpResponse.json({ + declaration: declared, + delegations: [reference], + children: [child], + }), + ); + const { client } = renderInApp(); + // Scoped to the declaration card: the delegations card carries the same + // sentence about its own read, so an unscoped match is ambiguous. + const card = await waitFor(() => { + const found = document.querySelector( + '[data-slot="declared-trigger"]', + ); + expect(found).not.toBeNull(); + return found as HTMLElement; + }); + await waitFor(() => + expect(within(card).getByText(/don.t have permission/i)).toBeTruthy(), + ); + + denied = false; + await act(async () => { + await client.refetchQueries({ queryKey: ["source-trigger-declaration"] }); + }); + + await screen.findByText("saas_datasource_reconcile"); + expect( + within( + document.querySelector('[data-slot="declared-trigger"]') as HTMLElement, + ).queryByText(/don.t have permission/i), + ).toBeNull(); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx index 86e603c11..b0f47d3d9 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx @@ -186,6 +186,11 @@ function TriggerIdentityPanel({ const outcome = readOutcome(read.isError, read.error); const withheld = !mayKeepRetainedRows(outcome); const declaration = withheld ? undefined : read.data?.declaration; + // Only meaningful when an answer survived: a refusal has nothing to be + // stale about, having just been deleted at the query boundary. + const declarationStale = declaration + ? staleReadNotice(outcome, "this trigger") + : null; const references = withheld ? [] : (read.data?.delegations ?? []); const children = withheld ? [] : (read.data?.children ?? []); const live = references.filter((reference) => reference.active); @@ -201,8 +206,34 @@ function TriggerIdentityPanel({ host still runs the schedule itself as a stopgap. - - {declaration?.undeclarableReason ? ( + + {/* + * THE CARD SAYS WHICH STATE THE READ IS IN (issue #1121, r22 + * F1). It withheld refused rows already, which is right, and + * then rendered every fact as an em dash — which reads as + * "this host declares nothing" to a viewer who is simply not + * allowed to see it. A refusal and a failure are now said in + * the card they are about; a transient failure keeps the + * prior answer under a stale notice. + */} + + {declarationStale && ( +

+ {declarationStale} +

+ )} + {!declaration && !read.isFetching ? ( +

+ {readOutcomeMessage( + outcome, + "this source's declared trigger", + "This source declares no trigger.", + )} +

+ ) : declaration?.undeclarableReason ? (

{declaration.undeclarableReason}

From 90733da7ad4453400e6996d23ce4d37c5324779f Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:36:11 -0400 Subject: [PATCH 14/20] fix(frontend): a refused read deletes the key it was refused, and no other MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parent is right about b3d01c3d3, and the defect is worse than a scope wobble. `getQueryCache().findAll({ queryKey })` matches by PREFIX in TanStack Query, so clearing `["source-trigger-declaration"]` reached every organization and every source. One source's 403 blanked a sibling source whose own read had SUCCEEDED — and nothing on screen said so, because that sibling's query is still `success` with no error: its row has no refusal to report, falls through to "This source declares no trigger", and states as fact the absence of a declaration the viewer is entitled to see. A fix for a misleading display introduced a second one. Both queries now clear the single exact key that was refused, via `find({ queryKey, exact: true })`. A refusal is evidence about the key it answered. TWO ISOLATION REGRESSIONS at the two scopes a key carries, both failing first: - a sibling SOURCE in the same organization. Only source A is refetched, which is the whole point — refetching both would hide the defect, because B's own successful re-read would repopulate what A had just wiped. The first draft of this probe refetched everything and passed against the bug. - the same source id under another ORGANIZATION, which the refusal is not about at all. The second query carries the same pattern, so it gets the same fix: the module tenant identity read cleared every cached triple on a refusal. That page asks one triple at a time, so the harm was a needless refetch rather than a false claim — but it is the same rule, it was written in this same PR, and leaving a known-imprecise copy behind after the parent named the class would be shipping the defect knowingly. It gets its own probe: another organization's cached answer survives a refusal. Mutation controls: restoring the prefix clear reds both declaration probes and the identity probe. The r22 closure tests all still pass unchanged — nothing in this narrowing weakens them, including 403→transient and 403→remount, which is what proves the narrower delete still deletes. Counts: unattended-work 24 → 26, module-tenant-identity 7 → 8. Frontend pure in full, serial: 337/337 files, 4830/4830, exit 0. Typecheck, biome, module-verify, naming gate, interface docs, test targets, story tests, boundaries, kit-version green. No backend change. No new mechanism: this narrows one call in each of two existing query functions. Co-Authored-By: Claude Opus 5 (1M context) --- .../module-tenant-identity/service/queries.ts | 23 ++- ...ant-identity-playground.container.test.tsx | 39 +++++ .../unattended-work/service/queries.ts | 20 ++- ...ource-trigger-schedules.container.test.tsx | 138 ++++++++++++++++++ 4 files changed, 212 insertions(+), 8 deletions(-) diff --git a/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts index 9d0664351..c759c353f 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts +++ b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts @@ -33,12 +33,27 @@ export function useModuleTenantIdentity( organizationId, }); } catch (error) { + // The refused triple, and only it. `findAll({ queryKey })` is a + // PREFIX match, so this cleared every cached triple — including + // answers about other bindings and other organizations that + // nothing had refused. This page asks one triple at a time, so + // the harm was a needless refetch rather than a false claim, but + // the rule is the same one the declaration query now follows + // (issue #1121): a refusal is evidence about the key it + // answered, and about no other. if (!mayKeepRetainedRows(readOutcome(true, error))) { - for (const query of client + client .getQueryCache() - .findAll({ queryKey: ["module-tenant-identity"] })) { - query.setState({ data: undefined, dataUpdatedAt: 0 }); - } + .find({ + queryKey: [ + "module-tenant-identity", + prefix, + binding, + organizationId, + ], + exact: true, + }) + ?.setState({ data: undefined, dataUpdatedAt: 0 }); } throw error; } diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx index 2bd868eec..1abb13ec2 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx @@ -235,3 +235,42 @@ describe("independent review: authority read outcomes", () => { expect(screen.getByText("audit-1")).toBeTruthy(); }); }); + +// AND THE REFUSAL IS ABOUT ONE TRIPLE (issue #1121, the same scope finding the +// declaration query carried). `findAll({ queryKey })` matches by prefix, so a +// refusal here deleted every cached triple — other bindings, other +// organizations — none of which had been refused. This page asks one triple at +// a time, so the harm was a needless refetch rather than a false claim; the rule +// is the same either way, and this pins it. +describe("a refusal's blast radius is the triple it answered", () => { + it("keeps another organization's cached answer", async () => { + serve(minted); + const { client } = renderInApp(); + fill(); + await screen.findByText("audit-1"); + + const other = [ + "module-tenant-identity", + "saas", + "module:audit", + "22222222-2222-4222-8222-222222222222", + ] as const; + client.setQueryData(other, minted); + + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + () => + HttpResponse.json( + { code: "permission_denied", message: "refused" }, + { status: 403 }, + ), + ), + ); + fireEvent.click(screen.getByRole("button", { name: "Resolve again" })); + await screen.findAllByText(/not allowed|permission|cannot view|access/i); + + expect(screen.queryByText("audit-1")).toBeNull(); + expect(client.getQueryData(other)).toBeTruthy(); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/service/queries.ts b/module/services/frontend/code/src/features/unattended-work/service/queries.ts index 31378617c..48eb7dce5 100644 --- a/module/services/frontend/code/src/features/unattended-work/service/queries.ts +++ b/module/services/frontend/code/src/features/unattended-work/service/queries.ts @@ -52,12 +52,24 @@ export function useSourceTriggerDeclaration(orgId: string, sourceId: string) { // answer under a stale notice, because blanking a good answer // because one request did not come back is worse than the // staleness (shared/lib/read-outcome.ts). + // + // AND IT CLEARS EXACTLY ONE KEY — the one that was refused. + // `findAll({ queryKey })` matches by PREFIX, so clearing + // `["source-trigger-declaration"]` reached every organization + // and every source: one source's 403 blanked a sibling source + // whose own read had succeeded. That sibling's query stays in + // `success` with no error, so its row has no refusal to report + // and says "This source declares no trigger" — a false + // statement about a source the viewer may see. A refusal is + // evidence about the key it answered, and about no other. if (!mayKeepRetainedRows(readOutcome(true, error))) { - for (const query of client + client .getQueryCache() - .findAll({ queryKey: ["source-trigger-declaration"] })) { - query.setState({ data: undefined, dataUpdatedAt: 0 }); - } + .find({ + queryKey: unattendedWorkKeys.declaration(orgId, sourceId), + exact: true, + }) + ?.setState({ data: undefined, dataUpdatedAt: 0 }); } throw error; } diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx index 7e947f454..ddbed32e8 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx @@ -781,3 +781,141 @@ describe("the declaration read's remaining states", () => { ); }); }); + +// A REFUSAL DELETES ONE DECLARATION, NOT EVERY DECLARATION (issue #1121, the +// parent's scope finding on b3d01c3d3). +// +// `getQueryCache().findAll({ queryKey })` matches by PREFIX in TanStack Query, +// so clearing `["source-trigger-declaration"]` reached every org and every +// source. One source refusing its read then blanked a sibling source whose own +// read had SUCCEEDED — and the damage is invisible at the component, because +// that sibling's query status is still `success` with no error: the row has no +// refusal to report and falls through to "This source declares no trigger", +// which is a false statement about a source the viewer is allowed to see. +// +// The blast radius of a refusal is the key that was refused. These two probes +// pin that at both scopes a key carries: another source in the same +// organization, and the same source in another organization. +const SOURCE_B = "aaaaaaaa-0000-4000-8000-000000000002"; + +const twoSources = { + datasources: [ + sources.datasources[0], + { + id: SOURCE_B, + orgId: "org-1", + provider: "DATASOURCE_PROVIDER_GITHUB", + github: { repo: "acme/runbooks", branch: "main" }, + status: "DATASOURCE_STATUS_ACTIVE", + }, + ], +}; + +const declaredB = { + ...declared, + schedule: { ...declared.schedule, cron: "15 * * * *" }, + identity: { source: SOURCE_B }, + digest: "9e22", +}; + +describe("one refused declaration does not blank another", () => { + it("leaves an authorized source's declaration intact when a sibling source is refused", async () => { + let denyA = false; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(twoSources), + ), + http.post( + rpc("DatasourceService", "GetSourceTriggerDeclaration"), + async ({ request }) => { + const body = (await request.json()) as { sourceId?: string }; + if (body.sourceId === SOURCE_B) { + return HttpResponse.json({ + declaration: declaredB, + delegations: [], + children: [], + }); + } + return denyA + ? HttpResponse.json({ code: "permission_denied" }, { status: 403 }) + : HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }); + }, + ), + ); + const { client } = renderInApp(); + await screen.findByText("*/30 * * * *"); + await screen.findByText("15 * * * *"); + + denyA = true; + // ONLY A IS REFETCHED, which is the whole point: B's query is not + // re-run, so if A's refusal deleted B's snapshot nothing restores it. + // Refetching both would hide the defect, because B's own successful + // re-read would repopulate what A had just wiped. + await act(async () => { + await client.refetchQueries({ + queryKey: ["source-trigger-declaration", "org-1", SOURCE], + exact: true, + }); + }); + + // A is gone and says why. + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); + expect(screen.queryByText("*/30 * * * *")).toBeNull(); + // B is UNTOUCHED: its read succeeded, so its declaration, its sweep row + // and its cached answer all stand. The last assertion is the one that + // fails on the prefix clear — the component would otherwise claim B + // declares no trigger while its query sits in `success`. + expect(screen.getByText("15 * * * *")).toBeTruthy(); + expect( + client.getQueryData(["source-trigger-declaration", "org-1", SOURCE_B]), + ).toBeTruthy(); + expect(screen.queryByText("This source declares no trigger.")).toBeNull(); + }); + + it("leaves another organization's declaration of the same source intact", async () => { + const other = ["source-trigger-declaration", "org-2", SOURCE] as const; + let denied = false; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + denied + ? HttpResponse.json({ code: "permission_denied" }, { status: 403 }) + : HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }), + ), + ); + const { client } = renderInApp(); + await screen.findByText("*/30 * * * *"); + + // A second organization's answer for the same source id, which no part + // of this refusal is about. + client.setQueryData(other, { + declaration: declaredB, + delegations: [], + children: [], + }); + + denied = true; + await act(async () => { + await client.refetchQueries({ + queryKey: ["source-trigger-declaration", "org-1", SOURCE], + }); + }); + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); + + expect(client.getQueryData(other)).toBeTruthy(); + }); +}); From 678d810d24a27ed5f2799769ccb205e9deb54a9f Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:46:38 -0400 Subject: [PATCH 15/20] fix(frontend): a superseded refusal is stale news, and a pending declaration claims no identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two confirmed findings at 90733da7a. Both are mine, both are read-state defects, and the second is a race my own clearing opened. 1. THE PENDING PLAYGROUND STILL CLAIMED AN ABSENT IDENTITY. The card learned to say which state its read was in and still fell into the facts list while the read was in flight, because the branch tested `!declaration && !read.isFetching` — true only once the read had FINISHED, so the whole pending window rendered the `
`: dashes, and an Identity row computed from no declaration at all reading "None — occurrences would be refused". That is the same false statement about authority the schedule row was fixed for, surviving in the other consumer of the shared query. The branch is now on HAVING a declaration, never on the read having ended, with the same never-flashing wait the schedule row uses; every fact in the list reads a declaration that exists. 2. A SUPERSEDED REFUSAL ERASED A NEWER SUCCESS, in both query families. Two fetches overlap on one key. The older is superseded — TanStack cancels it when the newer starts — but its request is already in flight and eventually rejects 403, and its catch then deleted the answer the NEWER fetch had just written. The query is left in a state no component reads correctly: status `success`, fetchStatus `idle`, `data` undefined, `error` null. The row has no refusal to report and no declaration to show, so it says "This source declares no trigger" about a source whose latest read succeeded. My fix for a misleading display opened a new way to produce one. The condition is exactly "no newer success replaced what this fetch was reading", and the lifecycle already records it: `dataUpdatedAt`. Each fetch captures it before the request and deletes only if it has not moved — the field the clear already writes, so no mechanism is added beside the query. A superseded refusal is stale news about authority, not current evidence. The deletion still happens when nothing newer replaced the answer, including with no component mounted — the case this guard must not swallow, and the one the reviewer named. Both families have an unmounted-refusal test proving the snapshot is gone. FAILING FIRST, then passing: the pending-playground probe ("expected
to be null"); both supersession probes ("expected undefined to be truthy"). Four mutation controls, each killing its own: deleting unconditionally in the declaration query (1 red) and in the identity query (1 red); never deleting at all (6 red — every refusal, resurrection and isolation test, which is what proves the guard did not quietly disable the deletion); the playground card as it stood at 90733da7a (1 red). Proven across both query families, as asked: pending, cold 403, cold non-refusal failure, success→403, superseded-old-403-after-newer-success, 403→transient, 403→remount, recovery on success, transient retained-and-stale, manual admissions preserved, per-key isolation at source and organization scope, and unmounted refusal. Counts: unattended-work 26 → 31, module-tenant-identity 8 → 10. Frontend pure in full, serial: 337/337 files, 4835/4835, exit 0. Typecheck, biome, module-verify, naming gate, interface docs, test targets, story tests, boundaries, kit-version green. No backend change. Co-Authored-By: Claude Opus 5 (1M context) --- .../module-tenant-identity/service/queries.ts | 28 ++-- ...ant-identity-playground.container.test.tsx | 91 +++++++++++- .../unattended-work/service/queries.ts | 36 ++++- ...ource-trigger-schedules.container.test.tsx | 133 ++++++++++++++++++ ...ttended-work-playground.container.test.tsx | 52 +++++++ .../ui/unattended-work-playground.tsx | 38 +++-- 6 files changed, 348 insertions(+), 30 deletions(-) diff --git a/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts index c759c353f..e6407644a 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts +++ b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts @@ -26,6 +26,18 @@ export function useModuleTenantIdentity( queryKey: ["module-tenant-identity", prefix, binding, organizationId], enabled: !!prefix && !!binding && !!organizationId, queryFn: async () => { + // The answer this fetch is reading, so a superseded refusal cannot + // delete a newer one (issue #1121; the same guard the declaration + // read carries, for the same race). + const key = [ + "module-tenant-identity", + prefix, + binding, + organizationId, + ] as const; + const readingAnswerFrom = + client.getQueryCache().find({ queryKey: key, exact: true })?.state + .dataUpdatedAt ?? 0; try { return await service.resolveModuleTenantIdentity({ prefix, @@ -42,18 +54,12 @@ export function useModuleTenantIdentity( // (issue #1121): a refusal is evidence about the key it // answered, and about no other. if (!mayKeepRetainedRows(readOutcome(true, error))) { - client + const entry = client .getQueryCache() - .find({ - queryKey: [ - "module-tenant-identity", - prefix, - binding, - organizationId, - ], - exact: true, - }) - ?.setState({ data: undefined, dataUpdatedAt: 0 }); + .find({ queryKey: key, exact: true }); + if (entry && entry.state.dataUpdatedAt <= readingAnswerFrom) { + entry.setState({ data: undefined, dataUpdatedAt: 0 }); + } } throw error; } diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx index 1abb13ec2..762503a59 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx @@ -1,4 +1,10 @@ -import { cleanup, fireEvent, screen, within } from "@testing-library/react"; +import { + act, + cleanup, + fireEvent, + screen, + within, +} from "@testing-library/react"; import { HttpResponse, http, type JsonBodyType } from "msw"; import { afterEach, describe, expect, it } from "vitest"; import { renderInApp, rpc } from "@/test/container"; @@ -274,3 +280,86 @@ describe("a refusal's blast radius is the triple it answered", () => { expect(client.getQueryData(other)).toBeTruthy(); }); }); + +// AND THE SAME SUPERSESSION RACE, in the other query family (issue #1121). +// +// Two fetches overlap on one triple. The older is superseded, answers last and +// answers 403, and its catch deleted the answer the NEWER fetch had written — +// leaving status `success`, fetchStatus `idle`, `data` undefined and `error` +// null, which no component can read correctly. The guard is the same: delete +// only when no newer success replaced what this fetch was reading. +describe("a superseded refusal in the identity read", () => { + const KEY = ["module-tenant-identity", "example", "operate", ORG] as const; + + it("does not erase the newer answer it was superseded by", async () => { + let call = 0; + let release403: (() => void) | undefined; + const held = new Promise((resolve) => { + release403 = resolve; + }); + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + async () => { + call++; + if (call === 1) return HttpResponse.json(minted); + if (call === 2) { + await held; + return HttpResponse.json( + { code: "permission_denied", message: "refused" }, + { status: 403 }, + ); + } + return HttpResponse.json(minted); + }, + ), + ); + const { client } = renderInApp(); + fill(); + await screen.findByText("audit-1"); + + const stale = client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + await act(async () => { + await client.refetchQueries({ queryKey: KEY, exact: true }); + }); + + release403?.(); + await act(async () => { + await stale; + }); + + expect(client.getQueryState([...KEY])?.status).toBe("success"); + expect(client.getQueryData([...KEY])).toBeTruthy(); + expect(screen.getByText("audit-1")).toBeTruthy(); + }); + + // The deletion still happens when nothing newer replaced it, unmounted. + it("still deletes a refusal that nothing newer replaced, with the view unmounted", async () => { + serve(minted); + const { client } = renderInApp(); + fill(); + await screen.findByText("audit-1"); + expect(client.getQueryData([...KEY])).toBeTruthy(); + + cleanup(); + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + () => + HttpResponse.json( + { code: "permission_denied", message: "refused" }, + { status: 403 }, + ), + ), + ); + await act(async () => { + await client + .refetchQueries({ queryKey: KEY, exact: true, type: "all" }) + .catch(() => undefined); + }); + + expect(client.getQueryData([...KEY])).toBeUndefined(); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/service/queries.ts b/module/services/frontend/code/src/features/unattended-work/service/queries.ts index 48eb7dce5..9198fff95 100644 --- a/module/services/frontend/code/src/features/unattended-work/service/queries.ts +++ b/module/services/frontend/code/src/features/unattended-work/service/queries.ts @@ -38,6 +38,15 @@ export function useSourceTriggerDeclaration(orgId: string, sourceId: string) { queryKey: unattendedWorkKeys.declaration(orgId, sourceId), enabled: !!orgId && !!sourceId, queryFn: async () => { + // What this fetch is reading, as the lifecycle already records it. + // A refusal may only delete the answer it was refused about; if a + // newer fetch has written one since, `dataUpdatedAt` has moved and + // this fetch's refusal is stale news (issue #1121). + const readingAnswerFrom = + client.getQueryCache().find({ + queryKey: unattendedWorkKeys.declaration(orgId, sourceId), + exact: true, + })?.state.dataUpdatedAt ?? 0; try { return await service.getSourceTriggerDeclaration({ orgId, sourceId }); } catch (error) { @@ -62,14 +71,27 @@ export function useSourceTriggerDeclaration(orgId: string, sourceId: string) { // and says "This source declares no trigger" — a false // statement about a source the viewer may see. A refusal is // evidence about the key it answered, and about no other. + // + // AND ONLY WHILE IT IS STILL THIS FETCH'S ANSWER TO DELETE. Two + // fetches overlap; the older is superseded, answers last and + // answers 403, and deleting here erased the newer fetch's + // success — leaving status `success`, fetchStatus `idle`, data + // undefined and error null, a state no component reads + // correctly: the row has no refusal to report and no + // declaration to show, so it says a source declares no trigger + // when its latest read succeeded. A superseded refusal is stale + // news about authority, not current evidence. The deletion + // still happens when nothing newer replaced the answer — + // including with no component mounted, which is the case this + // guard must not swallow. if (!mayKeepRetainedRows(readOutcome(true, error))) { - client - .getQueryCache() - .find({ - queryKey: unattendedWorkKeys.declaration(orgId, sourceId), - exact: true, - }) - ?.setState({ data: undefined, dataUpdatedAt: 0 }); + const entry = client.getQueryCache().find({ + queryKey: unattendedWorkKeys.declaration(orgId, sourceId), + exact: true, + }); + if (entry && entry.state.dataUpdatedAt <= readingAnswerFrom) { + entry.setState({ data: undefined, dataUpdatedAt: 0 }); + } } throw error; } diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx index ddbed32e8..6fe986c8e 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx @@ -919,3 +919,136 @@ describe("one refused declaration does not blank another", () => { expect(client.getQueryData(other)).toBeTruthy(); }); }); + +// A SUPERSEDED REFUSAL MUST NOT ERASE A NEWER SUCCESS (issue #1121). +// +// Two fetches overlap on one key. The older one is superseded — TanStack +// cancels it when the newer starts — but the request itself is already in +// flight and eventually rejects with 403. Its catch then ran and deleted the +// answer the NEWER fetch had just written, leaving the query in a state no +// component can read correctly: status `success`, fetchStatus `idle`, `data` +// undefined and `error` null. The row has no refusal to report and no +// declaration to show, so it says "This source declares no trigger" about a +// source whose latest read succeeded. +// +// The condition is the one the lifecycle already records: delete only if no +// newer success replaced what this fetch was reading. `dataUpdatedAt` is that +// fact, so the fix reads the field it already writes rather than adding a +// mechanism beside it. +describe("a superseded refusal", () => { + const KEY = ["source-trigger-declaration", "org-1", SOURCE] as const; + + function overlapping() { + let call = 0; + let release403: (() => void) | undefined; + const held = new Promise((resolve) => { + release403 = resolve; + }); + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(twoSources), + ), + http.post( + rpc("DatasourceService", "GetSourceTriggerDeclaration"), + async ({ request }) => { + const body = (await request.json()) as { sourceId?: string }; + if (body.sourceId !== SOURCE) { + return HttpResponse.json({ + declaration: declaredB, + delegations: [], + children: [], + }); + } + call++; + if (call === 1) { + return HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }); + } + if (call === 2) { + // The OLD fetch: superseded, answers last, and answers 403. + await held; + return HttpResponse.json( + { code: "permission_denied" }, + { status: 403 }, + ); + } + // The NEWER fetch, which succeeds with a different answer. + return HttpResponse.json({ + declaration: { + ...declared, + schedule: { ...declared.schedule, cron: "45 * * * *" }, + }, + delegations: [], + children: [], + }); + }, + ), + ); + return () => release403?.(); + } + + it("does not erase the newer answer it was superseded by", async () => { + const release = overlapping(); + const { client } = renderInApp(); + await screen.findByText("*/30 * * * *"); + + // The old fetch starts and is left in flight. + const stale = client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + // The newer fetch supersedes it and lands. + await act(async () => { + await client.refetchQueries({ queryKey: KEY, exact: true }); + }); + await screen.findByText("45 * * * *"); + + // Now the superseded request finally answers 403. + release(); + await act(async () => { + await stale; + }); + + const state = client.getQueryState([...KEY]); + expect(state?.status).toBe("success"); + // The newer answer is still there, and still on screen. + expect(client.getQueryData([...KEY])).toBeTruthy(); + expect(screen.getByText("45 * * * *")).toBeTruthy(); + expect(screen.queryByText("This source declares no trigger.")).toBeNull(); + }); + + // The deletion itself must survive the guard: a refusal with nothing newer + // behind it still clears, including when no component is mounted to see it. + it("still deletes a refusal that nothing newer replaced, with the view unmounted", async () => { + let denied = false; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + denied + ? HttpResponse.json({ code: "permission_denied" }, { status: 403 }) + : HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }), + ), + ); + const { client } = renderInApp(); + await screen.findByText("*/30 * * * *"); + expect(client.getQueryData([...KEY])).toBeTruthy(); + + cleanup(); + denied = true; + await act(async () => { + await client + .refetchQueries({ queryKey: KEY, exact: true, type: "all" }) + .catch(() => undefined); + }); + + expect(client.getQueryData([...KEY])).toBeUndefined(); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx index 409f02f0e..5b2502773 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx @@ -341,3 +341,55 @@ describe("the playground's declaration card when the read does not succeed", () ).toBeNull(); }); }); + +// A READ IN FLIGHT CLAIMS NOTHING ABOUT THE IDENTITY EITHER (issue #1121). +// +// The card learned to say which state its read was in and still fell into the +// facts list while the read was in flight: the `
` rendered dashes, and the +// Identity row, computed from no declaration at all, said "None — occurrences +// would be refused". That is the same false statement about authority the +// schedule row was fixed for, surviving in the other consumer because the +// branch tested `!declaration && !read.isFetching` — true only once the read +// had finished, and so false for the whole of the pending window. +describe("the playground's declaration card while the read is in flight", () => { + it("claims no identity before the declaration arrives", async () => { + let release: (() => void) | undefined; + const gate = new Promise((resolve) => { + release = resolve; + }); + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post( + rpc("DatasourceService", "GetSourceTriggerDeclaration"), + async () => { + await gate; + return HttpResponse.json({ + declaration: declared, + delegations: [reference], + children: [], + }); + }, + ), + ); + renderInApp(); + + const card = await waitFor(() => { + const found = document.querySelector( + '[data-slot="declared-trigger"]', + ); + expect(found).not.toBeNull(); + return found as HTMLElement; + }); + // Nothing about the identity, and no facts spelled as dashes, while the + // host has not answered. + expect( + within(card).queryByText("None — occurrences would be refused"), + ).toBeNull(); + expect(within(card).queryByText("Identity")).toBeNull(); + + release?.(); + await screen.findByText("saas_datasource_reconcile"); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx index b0f47d3d9..3a6c5db66 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx @@ -32,6 +32,7 @@ import { TableHead, TableHeader, TableRow, + useLoadingPhase, } from "@/shared/ui"; import { sourceLabel } from "../model/source-label"; import { @@ -191,6 +192,10 @@ function TriggerIdentityPanel({ const declarationStale = declaration ? staleReadNotice(outcome, "this trigger") : null; + // The same never-flashing wait the schedule row uses, so the pending window + // is a state of its own rather than the tail of the empty one. + const declarationPhase = useLoadingPhase(read.isFetching && !declaration); + const waiting = declarationPhase.indicator || declarationPhase.quiet; const references = withheld ? [] : (read.data?.delegations ?? []); const children = withheld ? [] : (read.data?.children ?? []); const live = references.filter((reference) => reference.active); @@ -225,7 +230,17 @@ function TriggerIdentityPanel({ {declarationStale}

)} - {!declaration && !read.isFetching ? ( + {/* + * THE FACTS NEED A DECLARATION — the branch is on having + * one, never on the read having finished. Testing + * `!declaration && !read.isFetching` meant the pending + * window fell through to the facts list, which rendered + * dashes and an Identity row computed from no declaration + * at all: "None — occurrences would be refused", the exact + * false claim about authority the schedule row was fixed + * for (issue #1121). + */} + {declaration ? null : waiting ? null : (

{readOutcomeMessage( outcome, @@ -233,48 +248,49 @@ function TriggerIdentityPanel({ "This source declares no trigger.", )}

- ) : declaration?.undeclarableReason ? ( + )} + {!declaration ? null : declaration.undeclarableReason ? (

{declaration.undeclarableReason}

) : (
- +
From e62ebd13b54f99ab8891e8d0e4a181cb2aa04d83 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:51:54 -0400 Subject: [PATCH 16/20] fix(frontend): the supersession guard counts generations, not milliseconds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit r23 is right, and the guard I shipped an hour ago was wrong for the case it exists to cover. `dataUpdatedAt` is a MILLISECOND timestamp, so two successes in the same millisecond carry the same value: "has a newer success replaced what I was reading?" answered no, and the superseded refusal deleted the newer answer after all. Sub-millisecond spacing is not an exotic case here — it is exactly the spacing an overlapping refetch has, so the guard was least reliable in the race it was written for. `dataUpdateCount` is the lifecycle's own generation counter. It increments on every write and cannot tie, and it is already on the state both queries read, so this remains the existing lifecycle owning the condition rather than anything new beside it. FAILING FIRST with the clock frozen, in both query families: `vi.useFakeTimers({ toFake: ["Date"] })` plus `setSystemTime` fakes Date ALONE, so promises and MSW keep working while every `Date.now()` TanStack takes returns one instant. Each test asserts its own premise before the behaviour — equal `dataUpdatedAt`, and `dataUpdateCount` greater than one — because a test that did not establish the tie would prove nothing about the field choice. Both reported "expected undefined to be truthy" on the timestamp guard. Mutation controls: putting `dataUpdatedAt` back reds the equal-millisecond test in the declaration family and in the identity family, one each, and nothing else — the field is the whole difference. Never deleting at all reds 7, which is what keeps proving the guard has not quietly disabled the deletion it sits beside, the unmounted refusals among them. Counts: unattended-work 31 → 32, module-tenant-identity 10 → 11. Frontend pure in full, serial: 337/337 files, 4837/4837, exit 0. Typecheck, biome, module-verify, naming gate, interface docs, test targets, story tests, boundaries, kit-version green. No backend change. Co-Authored-By: Claude Opus 5 (1M context) --- .../module-tenant-identity/service/queries.ts | 14 +-- ...ant-identity-playground.container.test.tsx | 67 +++++++++++++- .../unattended-work/service/queries.ts | 20 ++-- ...ource-trigger-schedules.container.test.tsx | 91 +++++++++++++++++++ 4 files changed, 178 insertions(+), 14 deletions(-) diff --git a/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts index e6407644a..2416b3b47 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts +++ b/module/services/frontend/code/src/features/module-tenant-identity/service/queries.ts @@ -26,18 +26,20 @@ export function useModuleTenantIdentity( queryKey: ["module-tenant-identity", prefix, binding, organizationId], enabled: !!prefix && !!binding && !!organizationId, queryFn: async () => { - // The answer this fetch is reading, so a superseded refusal cannot - // delete a newer one (issue #1121; the same guard the declaration - // read carries, for the same race). + // Which generation of the answer this fetch is reading, so a + // superseded refusal cannot delete a newer one (issue #1121; the + // same guard the declaration read carries, for the same race, on + // the same field — a generation counter rather than a millisecond + // timestamp, which two successes can share). const key = [ "module-tenant-identity", prefix, binding, organizationId, ] as const; - const readingAnswerFrom = + const readingGeneration = client.getQueryCache().find({ queryKey: key, exact: true })?.state - .dataUpdatedAt ?? 0; + .dataUpdateCount ?? 0; try { return await service.resolveModuleTenantIdentity({ prefix, @@ -57,7 +59,7 @@ export function useModuleTenantIdentity( const entry = client .getQueryCache() .find({ queryKey: key, exact: true }); - if (entry && entry.state.dataUpdatedAt <= readingAnswerFrom) { + if (entry && entry.state.dataUpdateCount <= readingGeneration) { entry.setState({ data: undefined, dataUpdatedAt: 0 }); } } diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx index 762503a59..4061ef68a 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx @@ -6,7 +6,7 @@ import { within, } from "@testing-library/react"; import { HttpResponse, http, type JsonBodyType } from "msw"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { renderInApp, rpc } from "@/test/container"; import { server } from "@/test/setup"; import { ModuleTenantIdentityPlayground } from "./module-tenant-identity-playground"; @@ -363,3 +363,68 @@ describe("a superseded refusal in the identity read", () => { expect(client.getQueryData([...KEY])).toBeUndefined(); }); }); + +// AND THE SAME FIELD CHOICE HERE (issue #1121, r23): the guard counts +// generations, not milliseconds. `dataUpdatedAt` is a millisecond timestamp, so +// two successes in one millisecond tie — the spacing an overlapping refetch +// actually has — and the superseded refusal would delete the newer answer after +// all. Only Date is faked, so promises and MSW keep working while every +// `Date.now()` returns one instant. +describe("a superseded refusal when both identity answers share a millisecond", () => { + const KEY = ["module-tenant-identity", "example", "operate", ORG] as const; + + afterEach(() => vi.useRealTimers()); + + it("does not erase a newer success that landed in the same millisecond", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-10-13T12:00:00.000Z")); + + let call = 0; + let release403: (() => void) | undefined; + const held = new Promise((resolve) => { + release403 = resolve; + }); + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + async () => { + call++; + if (call === 1) return HttpResponse.json(minted); + if (call === 2) { + await held; + return HttpResponse.json( + { code: "permission_denied", message: "refused" }, + { status: 403 }, + ); + } + return HttpResponse.json(minted); + }, + ), + ); + const { client } = renderInApp(); + fill(); + await screen.findByText("audit-1"); + + const stale = client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + await act(async () => { + await client.refetchQueries({ queryKey: KEY, exact: true }); + }); + + // The premise, asserted: one timestamp, two generations. + const after = client.getQueryState([...KEY]); + expect(after?.dataUpdatedAt).toBe( + new Date("2026-10-13T12:00:00.000Z").getTime(), + ); + expect(after?.dataUpdateCount).toBeGreaterThan(1); + + release403?.(); + await act(async () => { + await stale; + }); + + expect(client.getQueryData([...KEY])).toBeTruthy(); + expect(client.getQueryState([...KEY])?.status).toBe("success"); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/service/queries.ts b/module/services/frontend/code/src/features/unattended-work/service/queries.ts index 9198fff95..191c344ef 100644 --- a/module/services/frontend/code/src/features/unattended-work/service/queries.ts +++ b/module/services/frontend/code/src/features/unattended-work/service/queries.ts @@ -38,15 +38,21 @@ export function useSourceTriggerDeclaration(orgId: string, sourceId: string) { queryKey: unattendedWorkKeys.declaration(orgId, sourceId), enabled: !!orgId && !!sourceId, queryFn: async () => { - // What this fetch is reading, as the lifecycle already records it. - // A refusal may only delete the answer it was refused about; if a - // newer fetch has written one since, `dataUpdatedAt` has moved and - // this fetch's refusal is stale news (issue #1121). - const readingAnswerFrom = + // WHICH GENERATION OF THE ANSWER THIS FETCH IS READING. A refusal + // may only delete the answer it was refused about; if a newer fetch + // has written one since, this fetch's refusal is stale news (issue + // #1121). + // + // The field is `dataUpdateCount`, the lifecycle's own counter, and + // NOT `dataUpdatedAt`: that is a millisecond timestamp, so two + // successes in the same millisecond tie and the guard reads a newer + // answer as the one it was reading — at exactly the sub-millisecond + // spacing an overlapping refetch has. A generation cannot tie. + const readingGeneration = client.getQueryCache().find({ queryKey: unattendedWorkKeys.declaration(orgId, sourceId), exact: true, - })?.state.dataUpdatedAt ?? 0; + })?.state.dataUpdateCount ?? 0; try { return await service.getSourceTriggerDeclaration({ orgId, sourceId }); } catch (error) { @@ -89,7 +95,7 @@ export function useSourceTriggerDeclaration(orgId: string, sourceId: string) { queryKey: unattendedWorkKeys.declaration(orgId, sourceId), exact: true, }); - if (entry && entry.state.dataUpdatedAt <= readingAnswerFrom) { + if (entry && entry.state.dataUpdateCount <= readingGeneration) { entry.setState({ data: undefined, dataUpdatedAt: 0 }); } } diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx index 6fe986c8e..bcdb9245a 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx @@ -1052,3 +1052,94 @@ describe("a superseded refusal", () => { expect(client.getQueryData([...KEY])).toBeUndefined(); }); }); + +// THE GENERATION OF A SUCCESS IS NOT ITS CLOCK READING (issue #1121, r23). +// +// The first guard compared `dataUpdatedAt`, a MILLISECOND timestamp. Two +// successes in the same millisecond carry the same value, so "has a newer +// success replaced what I was reading?" answered no, and the superseded +// refusal deleted the newer answer after all — the very bug the guard was +// added for, surviving at sub-millisecond spacing, which is exactly the +// spacing an overlapping refetch has. +// +// `dataUpdateCount` is the lifecycle's own generation counter: it increments on +// every write and cannot tie. This test freezes the clock so both successes +// share a timestamp, which is what makes it a test of the FIELD CHOICE rather +// than of the comparison. +describe("a superseded refusal when both answers share a millisecond", () => { + const KEY = ["source-trigger-declaration", "org-1", SOURCE] as const; + + afterEach(() => vi.useRealTimers()); + + it("does not erase a newer success that landed in the same millisecond", async () => { + // Only Date is faked: timers, promises and MSW keep working, while + // every `Date.now()` TanStack takes returns one instant. + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-10-13T12:00:00.000Z")); + + let call = 0; + let release403: (() => void) | undefined; + const held = new Promise((resolve) => { + release403 = resolve; + }); + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post( + rpc("DatasourceService", "GetSourceTriggerDeclaration"), + async () => { + call++; + if (call === 1) { + return HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }); + } + if (call === 2) { + await held; + return HttpResponse.json( + { code: "permission_denied" }, + { status: 403 }, + ); + } + return HttpResponse.json({ + declaration: { + ...declared, + schedule: { ...declared.schedule, cron: "45 * * * *" }, + }, + delegations: [], + children: [], + }); + }, + ), + ); + const { client } = renderInApp(); + await screen.findByText("*/30 * * * *"); + + const stale = client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + await act(async () => { + await client.refetchQueries({ queryKey: KEY, exact: true }); + }); + + // The premise of the test, asserted rather than assumed: the newer + // success carries the SAME timestamp as the answer the old fetch was + // reading, and a later generation. + const after = client.getQueryState([...KEY]); + expect(after?.dataUpdatedAt).toBe( + new Date("2026-10-13T12:00:00.000Z").getTime(), + ); + expect(after?.dataUpdateCount).toBeGreaterThan(1); + + release403?.(); + await act(async () => { + await stale; + }); + + expect(client.getQueryData([...KEY])).toBeTruthy(); + expect(client.getQueryState([...KEY])?.status).toBe("success"); + }); +}); From 08273d385ba2f589391bc5a9a9051ac1724425b9 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:56:55 -0400 Subject: [PATCH 17/20] test(frontend): the deletion holds on the two lifecycle paths the test harness hid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit r23's request-order note names two paths the generation guard has to survive, and neither was in my suite. The guard itself is already right — the note's own updated-source check records all five of its probes passing against the `dataUpdateCount` version in e62ebd13b — so this adds the coverage, in both query families, rather than changing behaviour. 1. AN OBSERVER THAT UNMOUNTS WHILE THE REFUSAL IS IN FLIGHT. My existing unmounted test starts the refetch AFTER cleanup, on an already-inactive query. Removing the last observer mid-fetch is a different route — TanStack's `removeObserver`, which can cancel and revert the in-flight fetch's state — and a guard that read a reverted generation wrongly would stop deleting exactly there: with no component mounted to show anything, and the next mount serving the refused answer from cache. 2. RETRY, which the real client has and the harness does not: `lib/providers.tsx` sets `retry: 1`, `test/container.tsx` sets `retry: false`. So every refusal test in both files had been exercising a single attempt, and the retried shape — queryFn entered twice for one logical read, each attempt capturing its own generation — was untested. Three attempts are observed for the refused sequence, and the snapshot is gone after them rather than restored by the second attempt's own capture. Mutation controls: never deleting reds 15, the four new deletion tests among them. Deleting on every failure rather than only refusals reds the two transient-retention tests. One of the six new tests is NOT mutation-proven, and I would rather say so than let the count imply otherwise: "keeps the answer a retry recovered after a transient failure" survives both mutants, because the retry's own success rewrites the answer either way. It is a guard against a future change that blanks a recovered answer, not evidence of a sensitivity today. Counts: unattended-work 32 → 33, module-tenant-identity 11 → 14. Frontend pure in full, serial: 337/337 files, 4843/4843, exit 0. Typecheck, biome, module-verify, naming gate, interface docs, test targets, story tests, boundaries, kit-version green. No production code change in this commit. Co-Authored-By: Claude Opus 5 (1M context) --- ...ant-identity-playground.container.test.tsx | 105 +++++++++++++ ...ource-trigger-schedules.container.test.tsx | 141 ++++++++++++++++++ 2 files changed, 246 insertions(+) diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx index 4061ef68a..dbfe5c2b2 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx @@ -1,3 +1,4 @@ +import { QueryClient } from "@tanstack/react-query"; import { act, cleanup, @@ -428,3 +429,107 @@ describe("a superseded refusal when both identity answers share a millisecond", expect(client.getQueryState([...KEY])?.status).toBe("success"); }); }); + +// THE SAME TWO LIFECYCLE PATHS HERE (issue #1121, r23's request-order note): +// an observer that unmounts WHILE the refusal is in flight — TanStack's +// `removeObserver` route, which may cancel and revert the fetch's state, and is +// not the same path as refetching an already-inactive query — and RETRY, which +// the real client has (`src/lib/providers.tsx`: `retry: 1`) and this harness +// does not (`src/test/container.tsx`: `retry: false`), so every refusal test +// here had been exercising a single attempt. +describe("the identity deletion under the lifecycle paths the harness hides", () => { + const KEY = ["module-tenant-identity", "example", "operate", ORG] as const; + + it("still deletes when the observer unmounts before the refusal arrives", async () => { + let denied = false; + let release403: (() => void) | undefined; + const held = new Promise((resolve) => { + release403 = resolve; + }); + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + async () => { + if (!denied) return HttpResponse.json(minted); + await held; + return HttpResponse.json( + { code: "permission_denied", message: "refused" }, + { status: 403 }, + ); + }, + ), + ); + const { client, unmount } = renderInApp(); + fill(); + await screen.findByText("audit-1"); + + denied = true; + const refused = client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + unmount(); + release403?.(); + await act(async () => { + await refused; + }); + + expect(client.getQueryData([...KEY])).toBeUndefined(); + }); + + it("deletes a refused read that the client retried", async () => { + let attempts = 0; + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + () => { + attempts++; + return attempts === 1 + ? HttpResponse.json(minted) + : HttpResponse.json( + { code: "permission_denied", message: "refused" }, + { status: 403 }, + ); + }, + ), + ); + const client = new QueryClient({ + defaultOptions: { queries: { retry: 1, retryDelay: 0 } }, + }); + renderInApp(, client); + fill(); + await screen.findByText("audit-1"); + + await act(async () => { + await client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + }); + + expect(attempts).toBeGreaterThan(2); + expect(client.getQueryData([...KEY])).toBeUndefined(); + }); + + it("keeps the answer a retry recovered after a transient failure", async () => { + let attempts = 0; + server.use( + http.post( + rpc("PlatformAdminService", "ResolveModuleTenantIdentity"), + () => { + attempts++; + return attempts === 1 + ? HttpResponse.json({ message: "boom" }, { status: 500 }) + : HttpResponse.json(minted); + }, + ), + ); + const client = new QueryClient({ + defaultOptions: { queries: { retry: 1, retryDelay: 0 } }, + }); + renderInApp(, client); + fill(); + + await screen.findByText("audit-1"); + expect(attempts).toBe(2); + expect(client.getQueryData([...KEY])).toBeTruthy(); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx index bcdb9245a..f35a5852d 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx @@ -1,4 +1,5 @@ import { notifySourceReconcileAdmitted } from "@codefly-dev/saas-ui"; +import { QueryClient } from "@tanstack/react-query"; import { act, cleanup, screen, waitFor, within } from "@testing-library/react"; import { HttpResponse, http, type JsonBodyType } from "msw"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -1143,3 +1144,143 @@ describe("a superseded refusal when both answers share a millisecond", () => { expect(client.getQueryState([...KEY])?.status).toBe("success"); }); }); + +// THE TWO LIFECYCLE PATHS MY OWN SUITE HAD MISSED (issue #1121, r23's +// request-order note). Both were named by the reviewer against the generation +// guard, and both are about the guard NOT swallowing the deletion it sits +// beside. +// +// 1. An observer that unmounts WHILE the refusal is in flight. This is not the +// same path as refetching an already-inactive query: removing the last +// observer mid-fetch takes TanStack's `removeObserver` route, which may +// cancel and revert the in-flight fetch's state. A guard that compared the +// reverted generation wrongly would stop deleting exactly here — where no +// component is mounted to show anything, and the next mount would serve the +// refused answer from cache. +// +// 2. RETRY, which the real client has and this test harness does not: +// `src/lib/providers.tsx` sets `retry: 1`, `src/test/container.tsx` sets +// `retry: false`. So every refusal test in this file has been exercising a +// single attempt, and the retried shape — queryFn entered twice for one +// logical read, each attempt capturing its own generation — was untested. +describe("the deletion under the lifecycle paths the harness hides", () => { + const KEY = ["source-trigger-declaration", "org-1", SOURCE] as const; + + function retrying() { + return new QueryClient({ + defaultOptions: { queries: { retry: 1, retryDelay: 0 } }, + }); + } + + it("still deletes when the observer unmounts before the refusal arrives", async () => { + let denied = false; + let release403: (() => void) | undefined; + const held = new Promise((resolve) => { + release403 = resolve; + }); + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post( + rpc("DatasourceService", "GetSourceTriggerDeclaration"), + async () => { + if (!denied) { + return HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }); + } + await held; + return HttpResponse.json( + { code: "permission_denied" }, + { status: 403 }, + ); + }, + ), + ); + const { client, unmount } = renderInApp( + , + ); + await screen.findByText("*/30 * * * *"); + + // The refusal starts while the view is mounted... + denied = true; + const refused = client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + // ...and the view goes away before it answers. + unmount(); + release403?.(); + await act(async () => { + await refused; + }); + + expect(client.getQueryData([...KEY])).toBeUndefined(); + }); + + it("deletes a refused read that the client retried", async () => { + let attempts = 0; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => { + attempts++; + return attempts === 1 + ? HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }) + : HttpResponse.json({ code: "permission_denied" }, { status: 403 }); + }), + ); + const client = retrying(); + renderInApp(, client); + await screen.findByText("*/30 * * * *"); + + await act(async () => { + await client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + }); + + // Two attempts for one logical read, and the snapshot is gone after + // them rather than restored by the second attempt's own capture. + expect(attempts).toBe(3); + expect(client.getQueryData([...KEY])).toBeUndefined(); + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); + }); + + // And the retry's happy path: a transient failure that succeeds on the + // retry must not lose the answer the retry just wrote. + it("keeps the answer a retry recovered after a transient failure", async () => { + let attempts = 0; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => { + attempts++; + if (attempts === 1) { + return HttpResponse.json({ message: "boom" }, { status: 500 }); + } + return HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }); + }), + ); + const client = retrying(); + renderInApp(, client); + + await screen.findByText("*/30 * * * *"); + expect(attempts).toBe(2); + expect(client.getQueryData([...KEY])).toBeTruthy(); + }); +}); From 95b6259d05b17b086e7281b70b5c5b7b64dbe534 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:02:43 -0400 Subject: [PATCH 18/20] fix(frontend): a paused read has not answered, in all three views that read one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The read-lifecycle review's P2 is real and it is the same mistake in a third guise. Offline, TanStack parks a cold query at status `pending` with fetchStatus `paused` — and `isFetching` is FALSE, because that flag means "a request is on the wire", not "an answer is still coming". Every gate I wrote in this series was built on `isFetching`, so a paused read read as "finished, nothing came back": - `SourceTriggerSchedules` said "This source declares no trigger" about a source nobody had asked the host about yet — with the source list beside it rendering happily from cache, which is what makes the sentence so convincing. - the unattended-work playground's declared-trigger card said the same. - the identity playground asked an operator to "Complete the selection above" while all three fields were entered, which is worse than unhelpful: it blames the reader for the browser being offline. THE MODEL WAS ALREADY IN THESE FILES. The source list in both unattended-work views reads `sources.isPending && sources.fetchStatus !== "idle"` — pending with a non-idle fetch status is an answer still coming, whether fetching or paused; pending with an IDLE one is a read that was never issued, which is the genuine disabled case. All three views now reuse exactly that, and the identity page is the sharpest instance: its own comment already stated the rule ("a disabled query sits at isPending with an idle fetch status") while the code beneath it read the wrong flag. FAILING FIRST, through the library's own switch (`onlineManager.setOnline`) rather than a mocked flag, each asserting its premise — fetchStatus `paused`, status `pending` — before the behaviour, and each restoring the network and proving recovery: the schedules row, the playground card, and the identity triple. Mutation controls, the pair this fix has to pass: putting `isFetching` back reds all three paused probes and leaves the disabled-copy probe green. Collapsing the condition to `isPending` — treating every pending read as unanswered — reds the disabled-copy probes instead, 2 of them, which is what keeps "Complete the selection above" attached to an incomplete selection and nothing else. Retained/refused/superseded/unmount behaviour is untouched and re-run green: 51 tests across the two features, every one of the earlier guarantees among them. Counts: unattended-work 33 → 35, module-tenant-identity 14 → 16. Frontend pure in full, serial: 337/337 files, 4847/4847, exit 0. Typecheck, biome, module-verify, naming gate, interface docs, test targets, story tests, boundaries, kit-version green. No backend change. Co-Authored-By: Claude Opus 5 (1M context) --- ...ant-identity-playground.container.test.tsx | 64 ++++++++++++++++++- .../ui/module-tenant-identity-playground.tsx | 17 +++-- ...ource-trigger-schedules.container.test.tsx | 63 +++++++++++++++++- .../ui/source-trigger-schedules.tsx | 16 ++++- ...ttended-work-playground.container.test.tsx | 58 +++++++++++++++++ .../ui/unattended-work-playground.tsx | 14 ++-- 6 files changed, 218 insertions(+), 14 deletions(-) diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx index dbfe5c2b2..2dfc46ff8 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.container.test.tsx @@ -1,9 +1,10 @@ -import { QueryClient } from "@tanstack/react-query"; +import { onlineManager, QueryClient } from "@tanstack/react-query"; import { act, cleanup, fireEvent, screen, + waitFor, within, } from "@testing-library/react"; import { HttpResponse, http, type JsonBodyType } from "msw"; @@ -533,3 +534,64 @@ describe("the identity deletion under the lifecycle paths the harness hides", () expect(client.getQueryData([...KEY])).toBeTruthy(); }); }); + +// A PAUSED READ IS NOT AN INCOMPLETE SELECTION (issue #1121, the read-lifecycle +// review). Offline with all three fields entered, the query parks at status +// `pending` / fetchStatus `paused` and `isFetching` is FALSE — so the page fell +// through to "Complete the selection above to read issued capabilities" while +// the selection was complete. Its own comment already stated the rule the code +// did not follow: a disabled query sits at isPending with an IDLE fetch status, +// which is the genuine no-input case and keeps that copy. +describe("the identity read while paused by the browser being offline", () => { + afterEach(() => onlineManager.setOnline(true)); + + it("does not ask for a selection that is already complete, and recovers", async () => { + serve(minted); + onlineManager.setOnline(false); + const { client } = renderInApp(); + fill(); + + await waitFor(() => { + const state = client.getQueryState([ + "module-tenant-identity", + "example", + "operate", + ORG, + ]); + expect(state?.fetchStatus).toBe("paused"); + expect(state?.status).toBe("pending"); + }); + + expect( + screen.queryByText( + "Complete the selection above to read issued capabilities.", + ), + ).toBeNull(); + + onlineManager.setOnline(true); + await screen.findByText("audit-1"); + }); + + // The genuine no-input case must keep its own copy: a query disabled by an + // empty field sits at isPending with an IDLE fetch status. + it("still asks for the selection when a field is empty", async () => { + serve(minted); + const { client } = renderInApp(); + fireEvent.change(screen.getByLabelText("Module prefix"), { + target: { value: "example" }, + }); + + const state = client.getQueryState([ + "module-tenant-identity", + "example", + "", + "", + ]); + expect(state?.fetchStatus ?? "idle").toBe("idle"); + expect( + screen.getByText( + "Complete the selection above to read issued capabilities.", + ), + ).toBeTruthy(); + }); +}); diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx index 5b8907eab..2f5ee4d86 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx @@ -74,9 +74,16 @@ export function ModuleTenantIdentityPlayground() { const answer = read.data; // A disabled query is not a loading one: with a field empty it sits at // isPending with an idle fetch status, and a wait for a read that was never - // issued is a spinner that never resolves. - const phase = useLoadingPhase(read.isFetching); - const waiting = phase.indicator || phase.quiet; + // issued is a spinner that never resolves. That rule was stated here and + // then read off the wrong flag: `isFetching` means "a request is on the + // wire", so a read PAUSED because the browser is offline — pending, + // fetchStatus `paused`, isFetching false — fell through to "Complete the + // selection above" with all three fields entered (issue #1121). Pending + // with a non-idle fetch status is an answer still coming; pending with an + // IDLE one is the genuine no-input case, and keeps that copy. + const unanswered = read.isPending && read.fetchStatus !== "idle"; + const phase = useLoadingPhase(unanswered); + const waiting = unanswered || phase.indicator || phase.quiet; const stale = answer ? staleReadNotice( outcome, @@ -150,7 +157,7 @@ export function ModuleTenantIdentityPlayground() {
{stale && ( @@ -192,7 +199,7 @@ export function ModuleTenantIdentityPlayground() {
{waiting ? null : !answer ? ( diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx index f35a5852d..8208881c0 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx @@ -1,5 +1,5 @@ import { notifySourceReconcileAdmitted } from "@codefly-dev/saas-ui"; -import { QueryClient } from "@tanstack/react-query"; +import { onlineManager, QueryClient } from "@tanstack/react-query"; import { act, cleanup, screen, waitFor, within } from "@testing-library/react"; import { HttpResponse, http, type JsonBodyType } from "msw"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -1284,3 +1284,64 @@ describe("the deletion under the lifecycle paths the harness hides", () => { expect(client.getQueryData([...KEY])).toBeTruthy(); }); }); + +// A PAUSED READ HAS NOT ANSWERED (issue #1121, the read-lifecycle review). +// +// Offline, TanStack parks a cold query at status `pending` with fetchStatus +// `paused` — and `isFetching` is FALSE, because that flag means "a request is +// on the wire". Every gate in this file was written against `isFetching`, so a +// paused read read as "finished, nothing came back" and the row stated "This +// source declares no trigger" about a source nobody had asked the host about +// yet. The source LIST beside it already had this right: +// `sources.isPending && sources.fetchStatus !== "idle"`, which separates a read +// still coming (fetching or paused) from a read that was never issued +// (disabled, idle). That is the model these rows now reuse. +describe("a declaration read paused by the browser being offline", () => { + afterEach(() => onlineManager.setOnline(true)); + + it("claims nothing while the read is paused, and recovers when online", async () => { + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }), + ), + ); + // The list is already in the cache — the reported shape — so the panel + // renders its rows while the per-source read cannot leave the gate. + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + client.setQueryData(["unattended-sources", "org-1"], sources.datasources); + onlineManager.setOnline(false); + + renderInApp(, client); + await screen.findByText("acme/handbook"); + + // The premise, asserted rather than assumed. + await waitFor(() => { + const state = client.getQueryState([ + "source-trigger-declaration", + "org-1", + SOURCE, + ]); + expect(state?.fetchStatus).toBe("paused"); + expect(state?.status).toBe("pending"); + }); + + expect(screen.queryByText("This source declares no trigger.")).toBeNull(); + expect( + screen.queryByText("None — occurrences would be refused"), + ).toBeNull(); + expect(document.querySelector('[data-slot="host-sweep"]')).toBeNull(); + + // Back online, the read runs and the row fills in. + onlineManager.setOnline(true); + await screen.findByText("*/30 * * * *"); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx index 8cb09a223..a267b4023 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx @@ -177,8 +177,18 @@ function SourceTriggerRow({ const outcome = readOutcome(read.isError, read.error); const declaration = read.data?.declaration; const stale = declaration ? staleReadNotice(outcome, "this trigger") : null; - const phase = useLoadingPhase(read.isFetching); - const waiting = phase.indicator || phase.quiet; + // AN ANSWER IS STILL COMING, which is not the same as a request being on + // the wire. Offline, TanStack parks a cold query at status `pending` with + // fetchStatus `paused`, and `isFetching` is FALSE — so a gate on + // `isFetching` read a paused read as "finished, nothing came back" and the + // row stated this source declares no trigger about a read that never left + // the gate (issue #1121). The source list above already had the right + // model, and this is it: pending with a non-idle fetch status is a read + // still coming (fetching or paused); pending with an IDLE one is a read + // that was never issued, which is the genuine disabled case. + const unanswered = read.isPending && read.fetchStatus !== "idle"; + const phase = useLoadingPhase(unanswered); + const waiting = unanswered || phase.indicator || phase.quiet; const identity = useMemo(() => declaredIdentity(declaration), [declaration]); // The operation is read off the declaration so the manual rows name the // same string the scheduled ones do. A literal here would be this page's @@ -205,7 +215,7 @@ function SourceTriggerRow({ ) : null} {stale && ( diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx index 5b2502773..2352c95e5 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx @@ -1,3 +1,4 @@ +import { onlineManager, QueryClient } from "@tanstack/react-query"; import { act, cleanup, @@ -393,3 +394,60 @@ describe("the playground's declaration card while the read is in flight", () => await screen.findByText("saas_datasource_reconcile"); }); }); + +// AND THE SAME PAUSED READ IN THE PLAYGROUND'S CARD (issue #1121, the +// read-lifecycle review). Offline, a cold query parks at status `pending` with +// fetchStatus `paused`, and `isFetching` is FALSE — so the card's gate read it +// as "finished, nothing came back" and said this source declares no trigger +// about a read that never left the gate. The source list beside it already +// distinguishes paused from disabled with `isPending && fetchStatus !== "idle"`. +describe("the playground's declaration card when the read is paused", () => { + afterEach(() => onlineManager.setOnline(true)); + + it("claims nothing while paused, and recovers when online", async () => { + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + HttpResponse.json({ + declaration: declared, + delegations: [reference], + children: [], + }), + ), + ); + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + client.setQueryData(["unattended-sources", "org-1"], sources.datasources); + onlineManager.setOnline(false); + + renderInApp(, client); + const card = await waitFor(() => { + const found = document.querySelector( + '[data-slot="declared-trigger"]', + ); + expect(found).not.toBeNull(); + return found as HTMLElement; + }); + await waitFor(() => { + const state = client.getQueryState([ + "source-trigger-declaration", + "org-1", + SOURCE, + ]); + expect(state?.fetchStatus).toBe("paused"); + }); + + expect( + within(card).queryByText("This source declares no trigger."), + ).toBeNull(); + expect( + within(card).queryByText("None — occurrences would be refused"), + ).toBeNull(); + + onlineManager.setOnline(true); + await screen.findByText("saas_datasource_reconcile"); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx index 3a6c5db66..a8130c766 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx @@ -193,9 +193,15 @@ function TriggerIdentityPanel({ ? staleReadNotice(outcome, "this trigger") : null; // The same never-flashing wait the schedule row uses, so the pending window - // is a state of its own rather than the tail of the empty one. - const declarationPhase = useLoadingPhase(read.isFetching && !declaration); - const waiting = declarationPhase.indicator || declarationPhase.quiet; + // is a state of its own rather than the tail of the empty one — and on the + // same model as the source list beside it: pending with a non-idle fetch + // status is an answer still coming, whether a request is on the wire or + // PAUSED because the browser is offline. `isFetching` is false while paused, + // so a gate on it called a paused read finished (issue #1121). + const unanswered = read.isPending && read.fetchStatus !== "idle"; + const declarationPhase = useLoadingPhase(unanswered); + const waiting = + unanswered || declarationPhase.indicator || declarationPhase.quiet; const references = withheld ? [] : (read.data?.delegations ?? []); const children = withheld ? [] : (read.data?.children ?? []); const live = references.filter((reference) => reference.active); @@ -222,7 +228,7 @@ function TriggerIdentityPanel({ * prior answer under a stale notice. */} {declarationStale && ( From 174b6ad9ceb54efe44204abc846e2ad898e3561c Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:07:05 -0400 Subject: [PATCH 19/20] fix(frontend): the children card and the occurrence button stop asserting an absence they cannot see MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit r24's second half, and the same defect one layer down: two more surfaces read the SAME declaration response and reported an absence straight off an array length. - the children card said "No child has been minted against this source's references" whenever `children` was empty — so a held request, a paused read and a cold refusal all produced a statement about this source's children that the host had never made. - the occurrence button's tooltip said "No live reference: an occurrence would be refused" whenever `live` was empty, which is a claim about the host's answer. A read that has not answered has made none. A TRUE EMPTY IS ONLY KNOWABLE AFTER A SUCCESSFUL READ, so the empty sentence now waits for the read that licenses it (`read.isSuccess`), and every other state says which one it is through the same `readOutcomeMessage` / `DelayedLoading` seam the cards above already use. A transient failure keeps the rows: those children were really minted, and one request not coming back does not unmint them. THE BUTTON'S DISABLED STATE IS DELIBERATELY UNCHANGED. Refusing to act on a reference set this page cannot see is correct, and mutation authorization is not what this fix is about — only what the operator is told about why. The refused case asserts both halves: still disabled, and the explanation now names the refusal. Failing first, three reds: held request, cold 403, paused offline. Two cases passed from the start and are kept as the guard against over-correcting — a true empty after a successful read, and children retained through a transient failure. Mutation controls: the array-length branch restored reds 3; the unconditional tooltip restored reds 3; and suppressing the empty sentence in every state — the over-correction — reds the true-empty test, which is the control that keeps this fix from trading a false statement for a blank card. Counts: unattended-work 35 → 40. Frontend pure in full, serial: 337/337 files, 4852/4852, exit 0. Typecheck, biome, module-verify, naming gate, interface docs, test targets, story tests, boundaries, kit-version green. No backend change. The source list's semantics are untouched. Co-Authored-By: Claude Opus 5 (1M context) --- ...ttended-work-playground.container.test.tsx | 156 ++++++++++++++++++ .../ui/unattended-work-playground.tsx | 59 ++++++- 2 files changed, 206 insertions(+), 9 deletions(-) diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx index 2352c95e5..203403d3d 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx @@ -451,3 +451,159 @@ describe("the playground's declaration card when the read is paused", () => { await screen.findByText("saas_datasource_reconcile"); }); }); + +// THE CHILDREN CARD AND THE ACTION'S EXPLANATION READ THE SAME RESPONSE +// (issue #1121, r24). Both asserted an absence straight off an array length, +// so a read that had not answered — cold refusal, held request, paused offline +// — produced "No child has been minted against this source's references" and a +// tooltip reading "No live reference: an occurrence would be refused". Neither +// is a fact the host had supplied; a true empty is only knowable after a +// successful read. +// +// The button's disabled state is deliberately NOT part of this fix: refusing to +// act on an unknown reference set is correct. What changes is what the operator +// is TOLD about why. +const CHILDREN_EMPTY = + "No child has been minted against this source's references."; +const NO_LIVE_REFERENCE = "No live reference: an occurrence would be refused"; + +describe("the children card and the occurrence button on a read that has not answered", () => { + afterEach(() => onlineManager.setOnline(true)); + + function held() { + let release: (() => void) | undefined; + const gate = new Promise((resolve) => { + release = resolve; + }); + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post( + rpc("DatasourceService", "GetSourceTriggerDeclaration"), + async () => { + await gate; + return HttpResponse.json({ + declaration: declared, + delegations: [reference], + children: [child], + }); + }, + ), + ); + return () => release?.(); + } + + function refuse() { + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + HttpResponse.json({ code: "permission_denied" }, { status: 403 }), + ), + ); + } + + it("claims no absent child while the request is still held", async () => { + const release = held(); + renderInApp(); + await screen.findByText("acme/handbook"); + + expect(screen.queryByText(CHILDREN_EMPTY)).toBeNull(); + expect(screen.queryByTitle(NO_LIVE_REFERENCE)).toBeNull(); + + release(); + await screen.findByText("docstore"); + }); + + it("says a refused read was refused rather than reporting no children", async () => { + refuse(); + renderInApp(); + await screen.findByText("acme/handbook"); + + await waitFor(() => expect(screen.queryByText(CHILDREN_EMPTY)).toBeNull()); + expect(screen.queryByTitle(NO_LIVE_REFERENCE)).toBeNull(); + // The action stays refused — authorization is unchanged — and says why. + const run = screen.getByRole("button", { name: /Run one occurrence/ }); + expect(run.hasAttribute("disabled")).toBe(true); + expect(run.getAttribute("title")).toMatch(/permission/i); + }); + + it("claims no absent child while the read is paused offline", async () => { + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + HttpResponse.json({ + declaration: declared, + delegations: [reference], + children: [child], + }), + ), + ); + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + client.setQueryData(["unattended-sources", "org-1"], sources.datasources); + onlineManager.setOnline(false); + renderInApp(, client); + await screen.findByText("acme/handbook"); + + await waitFor(() => { + const state = client.getQueryState([ + "source-trigger-declaration", + "org-1", + SOURCE, + ]); + expect(state?.fetchStatus).toBe("paused"); + }); + expect(screen.queryByText(CHILDREN_EMPTY)).toBeNull(); + expect(screen.queryByTitle(NO_LIVE_REFERENCE)).toBeNull(); + + onlineManager.setOnline(true); + await screen.findByText("docstore"); + }); + + // A TRUE EMPTY IS STILL SAID — after a read that succeeded. Suppressing it + // everywhere would trade one false statement for a blank card. + it("reports no children once a successful read says so", async () => { + serve({ declaration: declared, delegations: [reference], children: [] }); + renderInApp(); + + expect(await screen.findByText(CHILDREN_EMPTY)).toBeTruthy(); + }); + + // And a transient failure keeps what the last good read said, under a + // notice, rather than reporting an absence it cannot see. + it("keeps the children a transient failure left behind", async () => { + let down = false; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + down + ? HttpResponse.json({ message: "boom" }, { status: 500 }) + : HttpResponse.json({ + declaration: declared, + delegations: [reference], + children: [child], + }), + ), + ); + const { client } = renderInApp(); + await screen.findByText("docstore"); + + down = true; + await act(async () => { + await client + .refetchQueries({ queryKey: ["source-trigger-declaration"] }) + .catch(() => undefined); + }); + + expect(screen.getByText("docstore")).toBeTruthy(); + expect(screen.queryByText(CHILDREN_EMPTY)).toBeNull(); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx index a8130c766..aa75799df 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx @@ -430,10 +430,24 @@ function TriggerIdentityPanel({ onError: (error) => toast.error(String(error)), }) } + // The DISABLED state is unchanged: refusing to act on a + // reference set this page cannot see is correct, and this + // change is not about what the button does. What changes is + // what the operator is TOLD — "No live reference" is a claim + // about the host's answer, and a read that has not answered + // has made none (issue #1121, r24). disabled={reconcile.isPending || live.length === 0} title={ live.length === 0 - ? "No live reference: an occurrence would be refused" + ? unanswered + ? "Still reading this source’s references" + : !read.isSuccess + ? readOutcomeMessage( + outcome, + "this source’s references", + "No live reference: an occurrence would be refused", + ) + : "No live reference: an occurrence would be refused" : undefined } > @@ -442,14 +456,23 @@ function TriggerIdentityPanel({ - {children.length === 0 ? ( -

- No child has been minted against this source's references. -

- ) : ( + {/* + * A TRUE EMPTY IS ONLY KNOWABLE AFTER A SUCCESSFUL READ. This + * card reads the same response as the declaration above it, and + * reported "no child has been minted" off an array length — so a + * held request, a paused read and a refusal all produced a + * statement about this source's children that the host had never + * made (issue #1121, r24). The empty sentence now waits for the + * read that licenses it; everything else says which state it is + * in. A transient failure keeps the rows, because those children + * were really minted and one request not coming back does not + * unmint them. + */} + + {children.length > 0 ? ( @@ -480,6 +503,24 @@ function TriggerIdentityPanel({ ))}
+ ) : unanswered ? null : read.isSuccess ? ( +

+ No child has been minted against this source's references. +

+ ) : ( +

+ {readOutcomeMessage( + outcome, + "the children this source minted", + "No child has been minted against this source’s references.", + )} +

)}
From 62e007ecbca54aa2daf112b7a1ed1c8105b98536 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:23:03 -0400 Subject: [PATCH 20/20] fix(frontend): "has this read answered?" is asked once, in the read seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit r24's paused-retry case is real, and it is the state my own deletion creates: initial success, a refetch refused so the query function deletes the retained answer, then the retry PAUSES offline. The query sits at status `success`, fetchStatus `paused`, data undefined, error null — answered once, holding nothing, still trying. `isPending` is false, so the amendment in 95b6259d0 read it as answered and both views said "This source declares no trigger" a third time. Six rounds of this have all been the same question asked wrong, so it is now asked once. `readHasNotAnswered` joins `readOutcome`, `mayKeepRetainedRows` and `staleReadNotice` in `shared/lib/read-outcome.ts` — the seam that already owns "why is this empty", extended rather than duplicated, as the reviewer authorised. Nothing in hand, nothing failed yet, a fetch not idle. Its three clauses each carry their weight, and each has a control: - `data === undefined` — with an answer in hand there is nothing to wait for; a refetch over it is `staleReadNotice`'s business. Dropping it reds the unit case for an answer in hand. - no `error` — a failure already answered must not be hidden by a retry. Dropping it reds that unit case. - `fetchStatus !== "idle"` — a disabled query is idle, and a read never issued is not in progress. Dropping it reds 3, the identity page's "complete the selection" among them. All four consumers now read it: the schedules row, the identity page, and the declaration/children/references cards, which share one response and were classified three different ways. THE THIRD CARD (r25): the references card tested `outcome === "empty"` — and `empty` means nothing has FAILED, not that the read succeeded — so a read in flight produced "No reference is bound to this source's triggers. An occurrence would be refused", both an absence and a prediction, neither of them the host's. It now waits for the read that licenses the sentence, exactly as the children card does. AND A CORRECTION TO MY OWN LAST COMMIT, which matters more than the fix. I had added a test asserting a refusal STAYS on screen while its retry is paused. It passed, and it was wrong twice over: the installed TanStack RESETS a query when a refetch starts, so an offline refetch over a refusal leaves status `pending`, fetchStatus `paused` and **error null** — the refusal is gone from the state entirely — and my assertion was reading un-flushed DOM. I verified the real state by instrumenting it, replaced the test with one that asserts what the library actually does, and the row is checked to substitute nothing in its place. So r24's "preserve existing errors on paused retries" is not reachable by that route in this version; the clause stays as a guard, unit-tested directly, and the PR body should say so rather than claim a behaviour no path reaches. Mutation controls: reverting both views to `isPending && fetchStatus !== "idle"` reds the two paused-retry probes; the three clause controls above; the references card's array-length branch and the children card's remain covered from the previous head. Counts: unattended-work 40 → 45, module-tenant-identity 16, read-outcome unit 30 → 36. Frontend pure in full, serial: 337/337 files, 4863/4863, exit 0. Typecheck, biome, module-verify, naming gate, interface docs, test targets, story tests, boundaries, kit-version green. No backend change. Source-list semantics and the occurrence button's disabled expression are untouched. Co-Authored-By: Claude Opus 5 (1M context) --- .../ui/module-tenant-identity-playground.tsx | 17 +-- ...ource-trigger-schedules.container.test.tsx | 143 ++++++++++++++++++ .../ui/source-trigger-schedules.tsx | 17 +-- ...ttended-work-playground.container.test.tsx | 121 +++++++++++++++ .../ui/unattended-work-playground.tsx | 49 ++++-- .../shared/lib/__tests__/read-outcome.test.ts | 78 ++++++++++ .../code/src/shared/lib/read-outcome.ts | 42 +++++ 7 files changed, 432 insertions(+), 35 deletions(-) diff --git a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx index 2f5ee4d86..c7255df0c 100644 --- a/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx +++ b/module/services/frontend/code/src/features/module-tenant-identity/ui/module-tenant-identity-playground.tsx @@ -5,6 +5,7 @@ import { KeyRound } from "lucide-react"; import { useState } from "react"; import { EmptyState } from "@/components/empty-state"; import { + readHasNotAnswered, readOutcome, readOutcomeMessage, staleReadNotice, @@ -72,16 +73,12 @@ export function ModuleTenantIdentityPlayground() { // list here, so the only thing it is asked is whether the answer arrived. const outcome = readOutcome(read.isError, read.error); const answer = read.data; - // A disabled query is not a loading one: with a field empty it sits at - // isPending with an idle fetch status, and a wait for a read that was never - // issued is a spinner that never resolves. That rule was stated here and - // then read off the wrong flag: `isFetching` means "a request is on the - // wire", so a read PAUSED because the browser is offline — pending, - // fetchStatus `paused`, isFetching false — fell through to "Complete the - // selection above" with all three fields entered (issue #1121). Pending - // with a non-idle fetch status is an answer still coming; pending with an - // IDLE one is the genuine no-input case, and keeps that copy. - const unanswered = read.isPending && read.fetchStatus !== "idle"; + // A disabled query is not a loading one, a paused one is not finished, and + // a refused refetch's paused retry is neither — asked once, in the read + // seam, rather than re-derived from whichever flag looked closest (issue + // #1121). A read that was never issued is idle, which is what keeps + // "complete the selection above" attached to an incomplete selection. + const unanswered = readHasNotAnswered(read); const phase = useLoadingPhase(unanswered); const waiting = unanswered || phase.indicator || phase.quiet; const stale = answer diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx index 8208881c0..349dccf5e 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.container.test.tsx @@ -1345,3 +1345,146 @@ describe("a declaration read paused by the browser being offline", () => { await screen.findByText("*/30 * * * *"); }); }); + +// THE STATE MY OWN DELETION CREATES (issue #1121, r24's paused-retry case). +// +// Initial success, then a refetch whose first attempt is refused: the query +// function deletes the retained answer, correctly. The retry then PAUSES +// because the browser is offline, and the query sits in a shape no single flag +// describes — status `success` (it had data once), fetchStatus `paused`, data +// undefined, error null (the fetch has not finished failing). `isPending` is +// FALSE, so `isPending && fetchStatus !== "idle"` reads it as answered, and the +// row said "This source declares no trigger" again. +// +// What is true of every one of these states is simpler than the states: there +// is nothing in hand, nothing has failed yet, and a fetch is not idle. That is +// the predicate, and it lives in the read seam beside the other three. +describe("a refused refetch whose retry is paused offline", () => { + const KEY = ["source-trigger-declaration", "org-1", SOURCE] as const; + + afterEach(() => onlineManager.setOnline(true)); + + it("claims nothing in the state the deletion leaves behind, and recovers", async () => { + let call = 0; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => { + call++; + if (call === 1) { + return HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }); + } + if (call === 2) { + // The refusal lands and the browser drops offline with it, so + // the retry has to pause rather than race. + onlineManager.setOnline(false); + return HttpResponse.json( + { code: "permission_denied" }, + { status: 403 }, + ); + } + return HttpResponse.json({ + declaration: declared, + delegations: [], + children: [], + }); + }), + ); + const client = new QueryClient({ + defaultOptions: { queries: { retry: 1, retryDelay: 0 } }, + }); + renderInApp(, client); + await screen.findByText("*/30 * * * *"); + + // NOT awaited: a paused retry never settles, so awaiting the refetch + // here would hang rather than observe the state it produces. + const refetching = client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + + // The premise, asserted exactly: this is the composite state, not a + // plain pending one. + await waitFor(() => { + const state = client.getQueryState([...KEY]); + expect(state?.status).toBe("success"); + expect(state?.fetchStatus).toBe("paused"); + expect(state?.data).toBeUndefined(); + expect(state?.error).toBeNull(); + }); + + expect(screen.queryByText("This source declares no trigger.")).toBeNull(); + expect( + screen.queryByText("None — occurrences would be refused"), + ).toBeNull(); + expect(screen.queryByText("*/30 * * * *")).toBeNull(); + + onlineManager.setOnline(true); + await act(async () => { + await refetching; + }); + await screen.findByText("*/30 * * * *"); + }); + + // r24 ALSO asks that an existing error survive a paused retry. In the + // installed TanStack it cannot, and the reason is worth recording where the + // next person will look: starting a refetch RESETS the query, so a refetch + // that then pauses leaves status `pending`, fetchStatus `paused`, data + // undefined and **error null** — the refusal is gone from the state, not + // hidden by a gate. Asserted below rather than described. + // + // So the row cannot keep showing a refusal it can no longer substantiate, + // and what matters is that it substitutes nothing: no "declares no + // trigger", no identity claim. The predicate's no-error clause stays as the + // guard for an error that IS in hand, which is unit-tested directly + // (`readHasNotAnswered` in shared/lib) because no app path reaches + // error-present with a non-idle fetch in this version. + it("claims nothing when an offline refetch resets a refusal to a paused read", async () => { + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => + HttpResponse.json({ code: "permission_denied" }, { status: 403 }), + ), + ); + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + renderInApp(, client); + await waitFor(() => + expect(screen.getByText(/don.t have permission/i)).toBeTruthy(), + ); + + onlineManager.setOnline(false); + await act(async () => { + await client + .refetchQueries({ queryKey: KEY, exact: true }) + .catch(() => undefined); + }); + // What the library actually does with the refusal. + const state = client.getQueryState([...KEY]); + expect(state?.status).toBe("pending"); + expect(state?.fetchStatus).toBe("paused"); + expect(state?.error).toBeNull(); + + // And the row asserts nothing in its place. + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 60)); + }); + expect(screen.queryByText("This source declares no trigger.")).toBeNull(); + expect( + screen.queryByText("None — occurrences would be refused"), + ).toBeNull(); + expect(document.querySelector('[data-slot="host-sweep"]')).toBeNull(); + // The manual-pass table is always there: it comes from this browser, + // not from the read. + expect( + document.querySelector('[data-slot="source-trigger"]'), + ).not.toBeNull(); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx index a267b4023..7587f7c47 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/source-trigger-schedules.tsx @@ -10,6 +10,7 @@ import { useEffect, useMemo, useState } from "react"; import { EmptyState } from "@/components/empty-state"; import { mayKeepRetainedRows, + readHasNotAnswered, readOutcome, readOutcomeMessage, staleReadNotice, @@ -177,16 +178,12 @@ function SourceTriggerRow({ const outcome = readOutcome(read.isError, read.error); const declaration = read.data?.declaration; const stale = declaration ? staleReadNotice(outcome, "this trigger") : null; - // AN ANSWER IS STILL COMING, which is not the same as a request being on - // the wire. Offline, TanStack parks a cold query at status `pending` with - // fetchStatus `paused`, and `isFetching` is FALSE — so a gate on - // `isFetching` read a paused read as "finished, nothing came back" and the - // row stated this source declares no trigger about a read that never left - // the gate (issue #1121). The source list above already had the right - // model, and this is it: pending with a non-idle fetch status is a read - // still coming (fetching or paused); pending with an IDLE one is a read - // that was never issued, which is the genuine disabled case. - const unanswered = read.isPending && read.fetchStatus !== "idle"; + // AN ANSWER IS STILL COMING, asked once in the read seam rather than + // re-derived here. `isPending && fetchStatus !== "idle"` was closer than + // `isFetching` and still missed the state a refused refetch leaves — status + // `success`, data deleted, retry paused — which is how this row said "no + // trigger is declared" a third time (issue #1121, r24). + const unanswered = readHasNotAnswered(read); const phase = useLoadingPhase(unanswered); const waiting = unanswered || phase.indicator || phase.quiet; const identity = useMemo(() => declaredIdentity(declaration), [declaration]); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx index 203403d3d..207281bc6 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.container.test.tsx @@ -607,3 +607,124 @@ describe("the children card and the occurrence button on a read that has not ans expect(screen.queryByText(CHILDREN_EMPTY)).toBeNull(); }); }); + +// ALL THREE CARDS READ ONE RESPONSE, SO ALL THREE ARE CLASSIFIED THE SAME WAY +// (issue #1121, r24/r25). The references card was the third: it tested +// `outcome === "empty"`, and `empty` from the classifier means "nothing has +// failed", NOT "the read succeeded" — so a pending read said "No reference is +// bound to this source's triggers. An occurrence would be refused." +// +// And the paused-RETRY state reaches these cards too: initial success, a +// refetch refused (the query function deletes the answer), the retry paused +// offline. Status `success`, fetchStatus `paused`, data undefined, error null. +const REFERENCES_EMPTY = + "No reference is bound to this source's triggers. An occurrence would be refused."; + +describe("the cards on one response, in the states that are not an answer", () => { + afterEach(() => onlineManager.setOnline(true)); + + it("claims no absent reference while the read is still in flight", async () => { + let release: (() => void) | undefined; + const gate = new Promise((resolve) => { + release = resolve; + }); + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post( + rpc("DatasourceService", "GetSourceTriggerDeclaration"), + async () => { + await gate; + return HttpResponse.json({ + declaration: declared, + delegations: [reference], + children: [child], + }); + }, + ), + ); + renderInApp(); + await screen.findByText("acme/handbook"); + + expect(screen.queryByText(REFERENCES_EMPTY)).toBeNull(); + + release?.(); + await screen.findByText("saas_datasource_reconcile"); + }); + + // The true empty is still said once a successful read says so. + it("reports no reference once a successful read says so", async () => { + serve({ declaration: declared, delegations: [], children: [] }); + renderInApp(); + + expect(await screen.findByText(REFERENCES_EMPTY)).toBeTruthy(); + }); + + it("claims nothing in the state a refused refetch and a paused retry leave", async () => { + let call = 0; + server.use( + http.post(rpc("DatasourceService", "ListSources"), () => + HttpResponse.json(sources), + ), + http.post(rpc("DatasourceService", "GetSourceTriggerDeclaration"), () => { + call++; + if (call === 1) { + return HttpResponse.json({ + declaration: declared, + delegations: [reference], + children: [child], + }); + } + if (call === 2) { + onlineManager.setOnline(false); + return HttpResponse.json( + { code: "permission_denied" }, + { status: 403 }, + ); + } + return HttpResponse.json({ + declaration: declared, + delegations: [reference], + children: [child], + }); + }), + ); + const client = new QueryClient({ + defaultOptions: { queries: { retry: 1, retryDelay: 0 } }, + }); + renderInApp(, client); + await screen.findByText("saas_datasource_reconcile"); + + const refetching = client + .refetchQueries({ + queryKey: ["source-trigger-declaration", "org-1", SOURCE], + exact: true, + }) + .catch(() => undefined); + + await waitFor(() => { + const state = client.getQueryState([ + "source-trigger-declaration", + "org-1", + SOURCE, + ]); + expect(state?.status).toBe("success"); + expect(state?.fetchStatus).toBe("paused"); + expect(state?.data).toBeUndefined(); + expect(state?.error).toBeNull(); + }); + + // None of the three cards asserts an absence in this state. + expect(screen.queryByText(REFERENCES_EMPTY)).toBeNull(); + expect(screen.queryByText(CHILDREN_EMPTY)).toBeNull(); + expect(screen.queryByText("This source declares no trigger.")).toBeNull(); + expect(screen.queryByTitle(NO_LIVE_REFERENCE)).toBeNull(); + + onlineManager.setOnline(true); + await act(async () => { + await refetching; + }); + await screen.findByText("saas_datasource_reconcile"); + }); +}); diff --git a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx index aa75799df..5f8356329 100644 --- a/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx +++ b/module/services/frontend/code/src/features/unattended-work/ui/unattended-work-playground.tsx @@ -8,6 +8,7 @@ import { OrgSelector } from "@/components/org-selector"; import { useAuth } from "@/lib/auth"; import { mayKeepRetainedRows, + readHasNotAnswered, readOutcome, readOutcomeMessage, staleReadNotice, @@ -192,13 +193,13 @@ function TriggerIdentityPanel({ const declarationStale = declaration ? staleReadNotice(outcome, "this trigger") : null; - // The same never-flashing wait the schedule row uses, so the pending window - // is a state of its own rather than the tail of the empty one — and on the - // same model as the source list beside it: pending with a non-idle fetch - // status is an answer still coming, whether a request is on the wire or - // PAUSED because the browser is offline. `isFetching` is false while paused, - // so a gate on it called a paused read finished (issue #1121). - const unanswered = read.isPending && read.fetchStatus !== "idle"; + // ALL THREE CARDS BELOW READ THIS ONE RESPONSE, so they are classified + // once, here, from the read seam's own predicate. Every per-card flag I + // reached for was a near miss: `isFetching` is false while paused, and + // `isPending` is false in the state a refused refetch leaves behind — + // status `success`, the answer deleted, the retry paused (issue #1121, + // r24/r25). + const unanswered = readHasNotAnswered(read); const declarationPhase = useLoadingPhase(unanswered); const waiting = unanswered || declarationPhase.indicator || declarationPhase.quiet; @@ -334,15 +335,33 @@ function TriggerIdentityPanel({ + {/* + * `outcome === "empty"` means nothing has FAILED, not that the + * read succeeded — so a read still in flight produced "No + * reference is bound … an occurrence would be refused", which + * is both an absence and a prediction, neither of them the + * host's (issue #1121, r25). The sentence now waits for the + * read that licenses it, exactly as the children card does. + */} + {references.length === 0 ? ( -

- {outcome === "empty" - ? "No reference is bound to this source's triggers. An occurrence would be refused." - : readOutcomeMessage(outcome, "this source's delegations", "")} -

+ unanswered ? null : ( +

+ {read.isSuccess + ? "No reference is bound to this source's triggers. An occurrence would be refused." + : readOutcomeMessage( + outcome, + "this source's delegations", + "", + )} +

+ ) ) : ( diff --git a/module/services/frontend/code/src/shared/lib/__tests__/read-outcome.test.ts b/module/services/frontend/code/src/shared/lib/__tests__/read-outcome.test.ts index 929f66905..653f91bcc 100644 --- a/module/services/frontend/code/src/shared/lib/__tests__/read-outcome.test.ts +++ b/module/services/frontend/code/src/shared/lib/__tests__/read-outcome.test.ts @@ -2,6 +2,7 @@ import { Code, ConnectError } from "@connectrpc/connect"; import { describe, expect, it } from "vitest"; import { mayKeepRetainedRows, + readHasNotAnswered, readOutcome, readOutcomeMessage, staleReadNotice, @@ -89,3 +90,80 @@ describe("what survives a failed refresh", () => { expect(staleReadNotice("empty", "this team's roles")).toBeNull(); }); }); + +// THE QUESTION BEFORE THE OUTCOME (issue #1121). `readOutcome` classifies a +// read that finished; this one says whether it has finished at all, and the +// five states below are the ones that reached production as "finished, nothing +// came back" — each turned by some card into an absence the host never +// reported. +describe("readHasNotAnswered", () => { + it("is true for a cold read on the wire", () => { + expect( + readHasNotAnswered({ + data: undefined, + error: null, + fetchStatus: "fetching", + }), + ).toBe(true); + }); + + it("is true for a cold read paused offline, where isFetching is false", () => { + expect( + readHasNotAnswered({ + data: undefined, + error: null, + fetchStatus: "paused", + }), + ).toBe(true); + }); + + // The state a refused refetch leaves: the refusal deleted the retained + // answer and the retry is paused, so the query reports status `success` + // with nothing in it and no error. `isPending` is false here. + it("is true once a refusal has deleted the answer and its retry is paused", () => { + expect( + readHasNotAnswered({ + data: undefined, + error: null, + fetchStatus: "paused", + }), + ).toBe(true); + }); + + it("is false with an answer in hand, refetching or not", () => { + expect( + readHasNotAnswered({ + data: { any: true }, + error: null, + fetchStatus: "fetching", + }), + ).toBe(false); + expect( + readHasNotAnswered({ + data: { any: true }, + error: null, + fetchStatus: "idle", + }), + ).toBe(false); + }); + + // A failure already answered stays on screen: a paused retry does not hide + // a refusal the reader has already been given. + it("is false once something has failed, even while a retry is paused", () => { + expect( + readHasNotAnswered({ + data: undefined, + error: new Error("permission denied"), + fetchStatus: "paused", + }), + ).toBe(false); + }); + + // A disabled query is idle: a read that was never issued is not in + // progress, which is what keeps a genuine "complete the selection" message. + it("is false for a disabled query, which is idle with nothing in it", () => { + expect( + readHasNotAnswered({ data: undefined, error: null, fetchStatus: "idle" }), + ).toBe(false); + }); +}); diff --git a/module/services/frontend/code/src/shared/lib/read-outcome.ts b/module/services/frontend/code/src/shared/lib/read-outcome.ts index df5e932c3..be9d8f1ad 100644 --- a/module/services/frontend/code/src/shared/lib/read-outcome.ts +++ b/module/services/frontend/code/src/shared/lib/read-outcome.ts @@ -87,3 +87,45 @@ export function staleReadNotice( ? `Couldn't refresh ${subject}, so this may be out of date.` : null; } + +/** + * Whether a read has produced no answer yet — so nothing may be asserted about + * what it would have said. + * + * `readOutcome` classifies a read that FINISHED. This answers the question + * before it, and every surface that states an absence needs both: a card may + * only say "there is nothing here" once a read succeeded and said so. + * + * No single query flag carries it. `isFetching` means a request is on the wire, + * so it is false while a read is PAUSED offline. `isPending` means no data has + * ever arrived, so it is false in the state a refused refetch leaves behind: the + * refusal deletes the retained answer (see `mayKeepRetainedRows`), its retry + * pauses, and the query sits at status `success` with `data` undefined and + * `error` null — answered once, holding nothing, and still trying. Each of + * those read as "finished, nothing came back", and the surfaces above them + * turned it into "no trigger is declared", "no child has been minted", "no + * reference is bound: an occurrence would be refused". + * + * What is true of all of them is simpler than the states themselves: nothing is + * in hand, nothing has failed yet, and a fetch is not idle. + * + * The three parts each carry their weight: + * - `data === undefined` — with an answer in hand there is nothing to wait for; + * a refetch over it is `staleReadNotice`'s business, not this one. + * - no `error` — a failure already answered must stay on screen. A paused retry + * does not hide a refusal the reader has already been given. + * - `fetchStatus !== "idle"` — a DISABLED query is idle, and a read that was + * never issued is not a read in progress. That is what keeps a genuine + * "complete the selection above" attached to an incomplete selection. + */ +export function readHasNotAnswered(read: { + data?: unknown; + error?: unknown; + fetchStatus: "fetching" | "paused" | "idle"; +}): boolean { + return ( + read.data === undefined && + (read.error ?? null) === null && + read.fetchStatus !== "idle" + ); +}