diff --git a/.changeset/browser-publishable-keys.md b/.changeset/browser-publishable-keys.md new file mode 100644 index 0000000..689a9bb --- /dev/null +++ b/.changeset/browser-publishable-keys.md @@ -0,0 +1,6 @@ +--- +'@gatepost/client': minor +'@gatepost/field': minor +--- + +Accept only publishable keys in a web page, as spec 0.4.0 asks. A browser client now also refuses a key that is neither a publishable key nor a secret key, with its own message. The field refuses such a key in the same way and raises `gatepost-error` with the new code `unsupported_key`. diff --git a/.changeset/core-react-spec-0-4-0.md b/.changeset/core-react-spec-0-4-0.md new file mode 100644 index 0000000..1ee2677 --- /dev/null +++ b/.changeset/core-react-spec-0-4-0.md @@ -0,0 +1,6 @@ +--- +'@gatepost/core': patch +'@gatepost/react': patch +--- + +The package implements spec 0.4.0. That version changes only which keys a browser client and the field accept, and leaves the grammar as it was, so every function gives the same results as before. diff --git a/CONTEXT.md b/CONTEXT.md index 4b82f5f..1340405 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -115,6 +115,10 @@ _Avoid_: private key, API secret An API key that can ship inside an app or a web page. It starts with `nipost_pk_`. _Avoid_: public key, client key +**Unsupported key**: +A key that is neither a publishable key nor a secret key. A browser client and a field refuse it. +_Avoid_: unknown key, invalid key + **Widget**: NIPOST's own postcode picker. Gatepost does not build widgets. _Avoid_: using this word for a Gatepost component diff --git a/packages/client/README.md b/packages/client/README.md index d7ed2f6..75eb6cd 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -52,7 +52,7 @@ result.valid; // true The examples use `FC-01-Z99-ZZ-01`, a synthetic postcode, and show the answers of Gatepost's mock server, which CI runs them against. The real gateway does not know this postcode. -Get a key from NIPOST's dashboard. The gateway refuses every call without a key. A key that starts with `nipost_test_` or `nipost_live_` is a secret key, so keep it on a server. The client refuses a secret key in a page that has a `document`. +Get a key from NIPOST's dashboard. The gateway refuses every call without a key. A key that starts with `nipost_test_` or `nipost_live_` is a secret key, so keep it on a server. In a page that has a `document`, the client accepts only a publishable key, which starts with `nipost_pk_`, and refuses any other key. Each call raises a `PostcodeError` when it cannot give a result. Its `code` is one of `invalid_input`, `unauthorized`, `insufficient_credits`, `origin_not_allowed`, `forbidden`, `rate_limited`, `server_error`, `unexpected_response`, `network_error` and `timeout`. @@ -109,7 +109,7 @@ The client also takes a `transport` option, a function like `fetch`, and each ca | ------------- | ---------------------------------------------- | | Node | 22 or later, or a current browser with `fetch` | | Module format | ESM only | -| Gatepost spec | 0.3.0 | +| Gatepost spec | 0.4.0 | ## Docs diff --git a/packages/client/etc/client.api.md b/packages/client/etc/client.api.md index bd0c4bd..6108b30 100644 --- a/packages/client/etc/client.api.md +++ b/packages/client/etc/client.api.md @@ -113,6 +113,6 @@ export interface ReverseUnit { } // @public -export const SPEC_VERSION = "0.3.0"; +export const SPEC_VERSION = "0.4.0"; ``` diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 9afe032..8e60130 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -18,6 +18,7 @@ import type { const PARALLEL_REQUESTS = 4; const AUTOCOMPLETE_TIMEOUT_MS = 15_000; const SECRET_KEY = /^nipost_(test|live)_/; +const PUBLISHABLE_KEY = 'nipost_pk_'; // The numeric options and the rule for each. A value that breaks its rule is a programmer error. const NUMBER_RULES = [ @@ -43,9 +44,19 @@ function checkOptions(options: ClientOptions): void { if (apiKey === '') { throw new RangeError('apiKey must not be empty.'); } - if (apiKey !== undefined && SECRET_KEY.test(apiKey.trim()) && 'document' in globalThis) { + if (apiKey === undefined || !('document' in globalThis)) { + return; + } + // A browser drops white space around a header value, so the check reads the trimmed key. + const key = apiKey.trim(); + if (SECRET_KEY.test(key)) { throw new TypeError('A web page must not hold a secret key. Use a publishable key here.'); } + if (!key.startsWith(PUBLISHABLE_KEY)) { + throw new TypeError( + `The key is not a publishable key. Use a key that starts with ${PUBLISHABLE_KEY} here.`, + ); + } } /** @@ -76,7 +87,7 @@ export class PostcodeClient { * and the cache time. Each one is optional. * @throws RangeError for a `timeoutMs` of 0 or less, a `maxRetries` that is not a whole * number of 0 or more, a `cacheTtlMs` that is negative or not finite, or an empty `apiKey`. - * @throws TypeError for a secret key in a web page. + * @throws TypeError for a key in a web page that is not a publishable key. */ constructor(options: ClientOptions = {}) { checkOptions(options); diff --git a/packages/client/src/version.ts b/packages/client/src/version.ts index 49cad0c..2b8cc7a 100644 --- a/packages/client/src/version.ts +++ b/packages/client/src/version.ts @@ -8,7 +8,7 @@ * ```ts * import { SPEC_VERSION } from '@gatepost/client'; * - * SPEC_VERSION; // '0.3.0' + * SPEC_VERSION; // '0.4.0' * ``` */ -export const SPEC_VERSION = '0.3.0'; +export const SPEC_VERSION = '0.4.0'; diff --git a/packages/client/test/client.test.ts b/packages/client/test/client.test.ts index d2c2475..699d2d2 100644 --- a/packages/client/test/client.test.ts +++ b/packages/client/test/client.test.ts @@ -114,6 +114,30 @@ describe('the options', () => { expect(() => new PostcodeClient({ apiKey: 'nipost_pk_live_abc' })).not.toThrow(); }); + it.each(['abc', 'nipost_abc', 'nipost_pk', 'NIPOST_PK_abc', 'x nipost_pk_abc'])( + 'refuses an unsupported key in a web page: %j', + (apiKey) => { + vi.stubGlobal('document', {}); + expect(() => new PostcodeClient({ apiKey })).toThrow(TypeError); + expect(() => new PostcodeClient({ apiKey })).toThrow( + 'The key is not a publishable key. Use a key that starts with nipost_pk_ here.', + ); + }, + ); + + it.each([' nipost_pk_abc', '\tnipost_pk_abc\n', '\r\nnipost_pk_abc \t'])( + 'allows a publishable key with surrounding white space in a web page: %j', + (apiKey) => { + vi.stubGlobal('document', {}); + expect(() => new PostcodeClient({ apiKey })).not.toThrow(); + }, + ); + + it('allows no key in a web page', () => { + vi.stubGlobal('document', {}); + expect(() => new PostcodeClient()).not.toThrow(); + }); + it.each([ ' nipost_live_abc', '\tnipost_live_abc', @@ -130,8 +154,9 @@ describe('the options', () => { ); }); - it('allows a secret key on a server, where no document exists', () => { + it('allows a secret key and an unsupported key on a server, where no document exists', () => { expect(() => new PostcodeClient({ apiKey: 'nipost_live_abc' })).not.toThrow(); + expect(() => new PostcodeClient({ apiKey: 'abc' })).not.toThrow(); }); it('gives two clients their own keys and their own caches (API-9)', async () => { diff --git a/packages/client/test/key-secrecy.test.ts b/packages/client/test/key-secrecy.test.ts index 16dc278..97218ba 100644 --- a/packages/client/test/key-secrecy.test.ts +++ b/packages/client/test/key-secrecy.test.ts @@ -124,6 +124,18 @@ describe('the API key in an error', () => { expectNoKey(thrown); }); + it('is not in the error that refuses an unsupported key in a web page', () => { + vi.stubGlobal('document', {}); + let thrown: unknown; + try { + new PostcodeClient({ apiKey: SECRET_PART }); + } catch (error) { + thrown = error; + } + expect(thrown).toBeInstanceOf(TypeError); + expectNoKey(thrown); + }); + it('is not in the error of an option that breaks its rule', () => { let thrown: unknown; try { diff --git a/packages/core/README.md b/packages/core/README.md index 26766e0..00c4605 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -107,7 +107,7 @@ The package checks the form of a postcode. It cannot tell whether a building has | ------------- | ----------------------------------- | | Node | 22 or later, or any current browser | | Module format | ESM only | -| Gatepost spec | 0.3.0 | +| Gatepost spec | 0.4.0 | ## Docs diff --git a/packages/core/etc/core.api.md b/packages/core/etc/core.api.md index 6482ed6..0c4fd87 100644 --- a/packages/core/etc/core.api.md +++ b/packages/core/etc/core.api.md @@ -68,7 +68,7 @@ export interface Segments { } // @public -export const SPEC_VERSION = "0.3.0"; +export const SPEC_VERSION = "0.4.0"; // @public export function stateName(stateCode: string): string | null; diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index df3a3ce..8b7a7ac 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -8,7 +8,7 @@ * ```ts * import { SPEC_VERSION } from '@gatepost/core'; * - * SPEC_VERSION; // '0.3.0' + * SPEC_VERSION; // '0.4.0' * ``` */ -export const SPEC_VERSION = '0.3.0'; +export const SPEC_VERSION = '0.4.0'; diff --git a/packages/field/README.md b/packages/field/README.md index 985b370..1419911 100644 --- a/packages/field/README.md +++ b/packages/field/README.md @@ -66,7 +66,7 @@ A user who types `fc 01 z99 zz 01` sends `postcode=FC-01-Z99-ZZ-01`. With no key ``` -Use a publishable key, which starts with `nipost_pk_`, and add your site to its allowed origins in NIPOST's dashboard. Anyone can read a key in a page, so the field refuses a secret key: it sends no request, and it logs one console error for you. A key holds a lookup level, and a call above it fails. The field then says that it could not check the postcode. See [`@gatepost/client`](https://github.com/gatepost-dev/js/tree/main/packages/client#readme). +Use a publishable key, which starts with `nipost_pk_`, and add your site to its allowed origins in NIPOST's dashboard. Anyone can read a key in a page, so the field refuses a secret key and any other key that is not a publishable key: it sends no request, and it logs one console error for you. A key holds a lookup level, and a call above it fails. The field then says that it could not check the postcode. See [`@gatepost/client`](https://github.com/gatepost-dev/js/tree/main/packages/client#readme). A postcode that the gateway does not know, or a failed request, never stops the form. The gateway's data is new, so check the postcode again on your server. @@ -96,11 +96,11 @@ A postcode that the gateway does not know, or a failed request, never stops the The events bubble, so a page can listen on the form. At level 2, `gatepost-confirm` gives the recent house address of the postcode. The field never shows it, and your page should not show it to the person who typed the postcode, who can have typed any postcode. -| Event | `detail` | -| ------------------ | ----------------------------------------------------------------------------------------------- | -| `gatepost-change` | `value`, `postcode`, `source` (`typed`, `pasted`, `suggestion` or `gps`) and `accuracyM` | -| `gatepost-confirm` | `lookup`, the result of the lookup | -| `gatepost-error` | `code`, an error code of `@gatepost/client`, or `secret_key`, `gps_denied` or `gps_unavailable` | +| Event | `detail` | +| ------------------ | ------------------------------------------------------------------------------------------------------------------ | +| `gatepost-change` | `value`, `postcode`, `source` (`typed`, `pasted`, `suggestion` or `gps`) and `accuracyM` | +| `gatepost-confirm` | `lookup`, the result of the lookup | +| `gatepost-error` | `code`, an error code of `@gatepost/client`, or `secret_key`, `unsupported_key`, `gps_denied` or `gps_unavailable` | ### Change the words and the look @@ -158,7 +158,7 @@ The parts `field`, `label`, `hint`, `input`, `message` and `button` take `::part | ------------- | ---------------------------------------------------------- | | Browser | a current Chrome, Edge or Firefox, or Safari 16.4 or later | | Module format | ESM only | -| Gatepost spec | 0.3.0 | +| Gatepost spec | 0.4.0 | The package exports `SPEC_VERSION`, the version of the Gatepost spec that it implements. A server can import the package: it defines the element only in a browser. diff --git a/packages/field/etc/field.api.md b/packages/field/etc/field.api.md index 29e6c97..ec09c26 100644 --- a/packages/field/etc/field.api.md +++ b/packages/field/etc/field.api.md @@ -30,10 +30,10 @@ export interface ErrorDetail { } // @public -export type FieldErrorCode = PostcodeErrorCode | "secret_key" | "gps_denied" | "gps_unavailable"; +export type FieldErrorCode = PostcodeErrorCode | "secret_key" | "unsupported_key" | "gps_denied" | "gps_unavailable"; // @public -export type MessageKey = "label" | "hint" | "empty" | "bad_character" | "bad_length" | "unknown_state" | "bad_lga" | "bad_area" | "bad_unit" | "suggestion" | "use_suggestion" | "legacy_accepted" | "legacy_rejected" | "valid" | "checking" | "not_found" | "confirmed" | "confirmed_place" | "check_failed" | "secret_key" | "use_location" | "locating" | "gps_coarse" | "gps_not_found" | "gps_denied" | "gps_unavailable"; +export type MessageKey = "label" | "hint" | "empty" | "bad_character" | "bad_length" | "unknown_state" | "bad_lga" | "bad_area" | "bad_unit" | "suggestion" | "use_suggestion" | "legacy_accepted" | "legacy_rejected" | "valid" | "checking" | "not_found" | "confirmed" | "confirmed_place" | "check_failed" | "secret_key" | "unsupported_key" | "use_location" | "locating" | "gps_coarse" | "gps_not_found" | "gps_denied" | "gps_unavailable"; // @public export type Messages = Readonly>; @@ -67,6 +67,6 @@ export const PostcodeFieldElement: { }; // @public -export const SPEC_VERSION = "0.3.0"; +export const SPEC_VERSION = "0.4.0"; ``` diff --git a/packages/field/scripts/smoke.mjs b/packages/field/scripts/smoke.mjs index 2970c6b..f74f067 100644 --- a/packages/field/scripts/smoke.mjs +++ b/packages/field/scripts/smoke.mjs @@ -10,7 +10,7 @@ for (const [entry, exports] of [ ['@gatepost/field', field], ['@gatepost/field/element', element], ]) { - if (exports.SPEC_VERSION !== '0.3.0' || typeof exports.PostcodeFieldElement !== 'function') { + if (exports.SPEC_VERSION !== '0.4.0' || typeof exports.PostcodeFieldElement !== 'function') { throw new Error(`${entry} gave ${Object.keys(exports).join(', ')} on a server.`); } } diff --git a/packages/field/src/element.ts b/packages/field/src/element.ts index b68e11f..18d18cf 100644 --- a/packages/field/src/element.ts +++ b/packages/field/src/element.ts @@ -170,7 +170,7 @@ class FieldElement extends ElementBase implements PostcodeFieldElement { #errorsShown = false; readonly #requests = new Requests(); #client: PostcodeClient | null = null; - #keyRefused = false; + #keyRefused: 'secret_key' | 'unsupported_key' | null = null; #lastText = ''; /** Builds the shadow tree. The browser calls it when it creates the element. */ @@ -379,20 +379,25 @@ class FieldElement extends ElementBase implements PostcodeFieldElement { const apiKey = this.#key; const baseUrl = this.getAttribute('base-url'); this.#client = null; - this.#keyRefused = false; + this.#keyRefused = null; if (apiKey === '') { return; } try { this.#client = new PostcodeClient(baseUrl === null ? { apiKey } : { apiKey, baseUrl }); } catch (error) { - // The client throws a TypeError for a secret key in a web page (SEC-1). + // The client throws a TypeError for each key but a publishable key in a web page (SEC-1). if (!(error instanceof TypeError)) { throw error; } - this.#keyRefused = true; + const secret = /^nipost_(test|live)_/.test(apiKey); + this.#keyRefused = secret ? 'secret_key' : 'unsupported_key'; // eslint-disable-next-line no-console -- spec/field.md asks for one error for the developer. - console.error('gatepost-postcode-field: api-key holds a secret key. Use a publishable key.'); + console.error( + secret + ? 'gatepost-postcode-field: api-key holds a secret key. Use a publishable key.' + : 'gatepost-postcode-field: api-key is not a publishable key. Use a publishable key.', + ); } } @@ -509,11 +514,12 @@ class FieldElement extends ElementBase implements PostcodeFieldElement { } async #lookUp(postcode: Postcode, asked: Asked): Promise { - if (this.#keyRefused) { - this.#requests.outcome = { state: 'error', note: { key: 'secret_key' } }; + const refused = this.#keyRefused; + if (refused !== null) { + this.#requests.outcome = { state: 'error', note: { key: refused } }; // The event waits for the render and for the change that caused the lookup. queueMicrotask(() => { - this.#raise('gatepost-error', { code: 'secret_key' }); + this.#raise('gatepost-error', { code: refused }); }); return; } @@ -567,10 +573,11 @@ class FieldElement extends ElementBase implements PostcodeFieldElement { if (fix.code !== null) { this.#raise('gatepost-error', { code: fix.code }); } - } else if (this.#keyRefused) { - // The key became a secret key while the field waited for the position. - this.#finish({ state: 'error', note: { key: 'secret_key' } }); - this.#raise('gatepost-error', { code: 'secret_key' }); + } else if (this.#keyRefused !== null) { + // The key became a refused key while the field waited for the position. + const code = this.#keyRefused; + this.#finish({ state: 'error', note: { key: code } }); + this.#raise('gatepost-error', { code }); } else { // The key is gone: nothing is sent, and the field shows the state that its text gives. this.#requests.end(); diff --git a/packages/field/src/spec-messages.ts b/packages/field/src/spec-messages.ts index fa953fa..e55a138 100644 --- a/packages/field/src/spec-messages.ts +++ b/packages/field/src/spec-messages.ts @@ -26,6 +26,7 @@ export type MessageKey = | 'confirmed_place' | 'check_failed' | 'secret_key' + | 'unsupported_key' | 'use_location' | 'locating' | 'gps_coarse' @@ -65,6 +66,7 @@ export const ENGLISH: Messages = { confirmed_place: 'We found this postcode: {locality}, {lga}, {state}.', check_failed: 'We could not check this postcode just now. You can still continue.', secret_key: 'We cannot check postcodes on this page. You can still continue.', + unsupported_key: 'We cannot check postcodes on this page. You can still continue.', use_location: 'Use my location', locating: 'Finding your location.', gps_coarse: 'We added part of your postcode from your location. Type the rest.', diff --git a/packages/field/src/types.ts b/packages/field/src/types.ts index d741299..ba0fcce 100644 --- a/packages/field/src/types.ts +++ b/packages/field/src/types.ts @@ -34,10 +34,11 @@ export interface ConfirmDetail { } /** - * Why the field raised `gatepost-error`: an error code of the client, a secret key in the page, - * or a location that the user refused or the device did not give. + * Why the field raised `gatepost-error`: an error code of the client, a secret key or an + * unsupported key in the page, or a location that the user refused or the device did not give. */ -export type FieldErrorCode = PostcodeErrorCode | 'secret_key' | 'gps_denied' | 'gps_unavailable'; +export type FieldErrorCode = + PostcodeErrorCode | 'secret_key' | 'unsupported_key' | 'gps_denied' | 'gps_unavailable'; /** * The detail of the event `gatepost-error`. diff --git a/packages/field/src/version.ts b/packages/field/src/version.ts index 6fcf3a0..9cb6bbd 100644 --- a/packages/field/src/version.ts +++ b/packages/field/src/version.ts @@ -8,7 +8,7 @@ * ```ts * import { SPEC_VERSION } from '@gatepost/field'; * - * SPEC_VERSION; // '0.3.0' + * SPEC_VERSION; // '0.4.0' * ``` */ -export const SPEC_VERSION = '0.3.0'; +export const SPEC_VERSION = '0.4.0'; diff --git a/packages/field/test/location.test.ts b/packages/field/test/location.test.ts index 7b938b3..b307d43 100644 --- a/packages/field/test/location.test.ts +++ b/packages/field/test/location.test.ts @@ -550,6 +550,23 @@ describe('a location request and the settings', () => { expect(error).toHaveBeenCalledTimes(1); }); + it('sends nothing and says so when the key is unsupported as the position arrives', async () => { + const place = holdDevice(); + const error = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const fetch = vi.spyOn(globalThis, 'fetch'); + const { form, field, location, message } = mount(FIELD); + const errors = listen(form); + await userEvent.click(location); + field.setAttribute('api-key', 'pk_abc'); + place(5); + await vi.waitFor(() => { + expect(errors).toEqual([{ code: 'unsupported_key' }]); + }); + expect(message()).toBe('We cannot check postcodes on this page. You can still continue.'); + expect(fetch).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalledTimes(1); + }); + it('sends nothing and shows the state of the text when the key is gone', async () => { const place = holdDevice(); const fetch = vi.spyOn(globalThis, 'fetch'); diff --git a/packages/field/test/lookup.test.ts b/packages/field/test/lookup.test.ts index bd980ac..f5ec895 100644 --- a/packages/field/test/lookup.test.ts +++ b/packages/field/test/lookup.test.ts @@ -421,3 +421,22 @@ describe('a secret key in the page', () => { expect(error).not.toHaveBeenCalled(); }); }); + +describe('an unsupported key in the page', () => { + it('sends no request, logs one error and raises unsupported_key', async () => { + const { requests } = scriptGateway(); + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const { form, input, location, message } = mount( + '', + ); + const errors = listen(form, 'gatepost-error'); + expect(location.hidden).toBe(true); + await userEvent.type(input, 'FC01Z99ZZ01'); + expect(message()).toBe('We cannot check postcodes on this page. You can still continue.'); + expect(errors).toEqual([{ code: 'unsupported_key' }]); + expect(consoleError).toHaveBeenCalledOnce(); + expect(String(consoleError.mock.calls[0]?.[0])).toContain('not a publishable key'); + expect(String(consoleError.mock.calls[0]?.[0])).not.toContain('pk_abc'); + expect(requests).toEqual([]); + }); +}); diff --git a/packages/mock-server/README.md b/packages/mock-server/README.md index 6259f00..bf0b43c 100644 --- a/packages/mock-server/README.md +++ b/packages/mock-server/README.md @@ -81,7 +81,7 @@ await mock.close(); | Requirement | Version | | ------------- | ------------------------------------------ | | Node | 22.22.2 or later 22.x, or 24.15.0 or later | -| Gatepost spec | 0.3.0 | +| Gatepost spec | 0.4.0 | ## Docs diff --git a/packages/react/README.md b/packages/react/README.md index 906c4b4..f66b9a4 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -95,7 +95,7 @@ The [README of `@gatepost/field`](https://github.com/gatepost-dev/js/tree/main/p | React | 18 or 19 | | Browser | as for `@gatepost/field` | | Module format | ESM only | -| Gatepost spec | 0.3.0 | +| Gatepost spec | 0.4.0 | The package exports `SPEC_VERSION`, the version of the Gatepost spec that it implements. diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index d9ad83e..8cbace7 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -34,6 +34,6 @@ export interface PostcodeFieldProps { } // @public -export const SPEC_VERSION = "0.3.0"; +export const SPEC_VERSION = "0.4.0"; ``` diff --git a/packages/react/src/version.ts b/packages/react/src/version.ts index cd83187..933c4a9 100644 --- a/packages/react/src/version.ts +++ b/packages/react/src/version.ts @@ -8,7 +8,7 @@ * ```ts * import { SPEC_VERSION } from '@gatepost/react'; * - * SPEC_VERSION; // '0.3.0' + * SPEC_VERSION; // '0.4.0' * ``` */ -export const SPEC_VERSION = '0.3.0'; +export const SPEC_VERSION = '0.4.0'; diff --git a/spec b/spec index db359cd..b8a2119 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit db359cde28a7c5c549f1a3069f20078f19f05a58 +Subproject commit b8a211972517f918dec519923df8b87c835c9ec3