@runbound/config-browser 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +161 -0
- package/README.md +136 -2
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/dist/config-context-client.d.ts +74 -0
- package/dist/config-context-client.js +1 -0
- package/dist/config-context.d.ts +55 -0
- package/dist/config-context.js +1 -0
- package/dist/config-delivery-store.d.ts +73 -0
- package/dist/config-delivery-store.js +1 -0
- package/dist/config-experiment-committed-render.d.ts +57 -0
- package/dist/config-experiment-committed-render.js +1 -0
- package/dist/config-experiment-web-durability.d.ts +85 -0
- package/dist/config-experiment-web-durability.js +1 -0
- package/dist/config-experiment-web-proof.d.ts +22 -0
- package/dist/config-experiment-web-proof.js +1 -0
- package/dist/config-experimentation.d.ts +308 -0
- package/dist/config-experimentation.js +1 -0
- package/dist/config-public-decision.d.ts +67 -0
- package/dist/config-public-decision.js +1 -0
- package/dist/config-refresh.d.ts +69 -0
- package/dist/config-refresh.js +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -0
- package/dist/internal/config-contract/base.d.ts +180 -0
- package/dist/internal/config-contract/core.d.ts +82 -0
- package/dist/internal/config-contract/decisioning.d.ts +430 -0
- package/dist/internal/config-contract/experimentation.d.ts +460 -0
- package/dist/internal/config-contract/schema.d.ts +133 -0
- package/package.json +74 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The coordinate `@runbound/config-browser` ships to npm.
|
|
4
|
+
|
|
5
|
+
`0.1.0-alpha.1` is the first release, under the one scheme every Runbound SDK
|
|
6
|
+
family uses: a single logical SemVer pre-release, written in each registry's
|
|
7
|
+
own syntax, under the Runbound Runtime and SDK Licence.
|
|
8
|
+
|
|
9
|
+
## 0.1.0-alpha.1
|
|
10
|
+
|
|
11
|
+
The first release, an unsupported alpha. [The README](./README.md) says what
|
|
12
|
+
the package does and what state it is in.
|
|
13
|
+
|
|
14
|
+
- Cloud mode is not yet available: Runbound Cloud does not yet deliver
|
|
15
|
+
managed Config.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Runbound Runtime and SDK Licence
|
|
2
|
+
|
|
3
|
+
Version 2026-10-19. Copyright Runbound Ltd. All rights reserved.
|
|
4
|
+
Licence identifier: LicenseRef-Runbound-Runtime-SDK.
|
|
5
|
+
|
|
6
|
+
## 1. Scope
|
|
7
|
+
|
|
8
|
+
Runbound Ltd (company 17482194, England and Wales) grants this licence for the
|
|
9
|
+
Runbound Runtime adapters and Config and Telemetry SDKs supplied with this
|
|
10
|
+
text, including their Runbound-authored dependencies, source and documentation
|
|
11
|
+
(the Software). Contact: legal@runbound.io; 71-75 Shelton Street, Covent Garden,
|
|
12
|
+
London, United Kingdom, WC2H 9JQ; +44 20 4577 3939.
|
|
13
|
+
|
|
14
|
+
The Software is proprietary. This licence does not cover the Runbound CLI,
|
|
15
|
+
Cloud service, API or dashboard. Those have their own terms. Third-party
|
|
16
|
+
components retain their own licences and notices; this licence does not reduce
|
|
17
|
+
any rights those licences give you.
|
|
18
|
+
|
|
19
|
+
## 2. Use in your applications
|
|
20
|
+
|
|
21
|
+
You may install, copy, run and integrate the Software to develop, test and
|
|
22
|
+
operate your own applications, including commercial applications, internal
|
|
23
|
+
tools, continuous integration and services you provide to your customers.
|
|
24
|
+
You may inspect and modify source supplied to you for that integration,
|
|
25
|
+
debugging and fixing defects, subject to section 4.
|
|
26
|
+
|
|
27
|
+
Standalone local capabilities documented by Runbound as free may be used
|
|
28
|
+
without an account, subscription, time limit or licence fee. Capabilities
|
|
29
|
+
that require Runbound Cloud may be used only with a valid entitlement to the
|
|
30
|
+
relevant Runbound service, including a free plan or trial when offered, and
|
|
31
|
+
within its terms and limits. Installing an SDK does not grant Cloud access or
|
|
32
|
+
make a paid service free. There is no separate SDK royalty or per-developer
|
|
33
|
+
licence fee for the use permitted here.
|
|
34
|
+
|
|
35
|
+
Your applications, data, policies and outputs remain yours. This licence does
|
|
36
|
+
not require you to disclose your application source, grant Runbound rights to
|
|
37
|
+
it, or license it under these terms. The industry you work in does not restrict
|
|
38
|
+
the application rights above.
|
|
39
|
+
|
|
40
|
+
## 3. Shipping applications
|
|
41
|
+
|
|
42
|
+
You may distribute copies of the Software, including integration changes
|
|
43
|
+
permitted by section 2, as a dependency of your own applications. This includes
|
|
44
|
+
containers, deployment bundles, browser bundles, mobile and desktop apps, and
|
|
45
|
+
source-based dependencies where the language requires them. You may keep
|
|
46
|
+
internal dependency mirrors and give copies to contractors operating your
|
|
47
|
+
applications on your behalf.
|
|
48
|
+
|
|
49
|
+
Include this licence and the applicable third-party notices with distributed
|
|
50
|
+
copies, in your normal dependency-notices location. Keep ownership notices;
|
|
51
|
+
you need not display a Runbound logo or add an attribution to your product UI.
|
|
52
|
+
Identify modifications to the Software in its accompanying notices. Do not
|
|
53
|
+
represent a modified copy as an official or signed Runbound release.
|
|
54
|
+
|
|
55
|
+
Recipients may use and redistribute the embedded Software as part of that
|
|
56
|
+
application under this licence. They may not extract it for separate
|
|
57
|
+
redistribution. If an application connects to Runbound Cloud, that connection
|
|
58
|
+
must be covered by the appropriate Runbound service entitlement; distributing
|
|
59
|
+
an application does not transfer an account or grant a service entitlement.
|
|
60
|
+
|
|
61
|
+
You may fork Runbound's public source repositories and publish proposed changes
|
|
62
|
+
solely to contribute to Runbound under the repository's contribution terms.
|
|
63
|
+
Keep this licence and the applicable notices. A contribution fork does not
|
|
64
|
+
permit standalone product releases or a substitute service.
|
|
65
|
+
|
|
66
|
+
## 4. Separate products and services
|
|
67
|
+
|
|
68
|
+
The permissions above do not allow you to distribute the Software, or a
|
|
69
|
+
modified or renamed version of it, as a standalone SDK, runtime, agent or
|
|
70
|
+
library. Nor may you use the Software's implementation to provide a replacement
|
|
71
|
+
for Runbound's runtime enforcement, configuration or telemetry service to
|
|
72
|
+
third parties. These activities require a separate written agreement with
|
|
73
|
+
Runbound. Wrapping or renaming the Software does not grant these rights.
|
|
74
|
+
|
|
75
|
+
This restriction concerns the Runbound implementation you copy or modify. It
|
|
76
|
+
does not restrict software you develop independently without copying that
|
|
77
|
+
implementation. Integrating the Software to protect your applications, retrieve
|
|
78
|
+
their configuration or collect their telemetry is permitted, including when
|
|
79
|
+
your applications themselves are commercial products or services.
|
|
80
|
+
|
|
81
|
+
Except for contribution forks permitted in section 3, do not sell, sublicense
|
|
82
|
+
or publicly mirror the Software separately from the application distribution
|
|
83
|
+
permitted in section 3. Do not remove licence or
|
|
84
|
+
ownership notices, bypass Cloud authorisation or entitlement checks, or claim
|
|
85
|
+
rights to Runbound's name or trademarks beyond truthful identification.
|
|
86
|
+
Nothing in this licence limits rights that applicable law does not allow us
|
|
87
|
+
to restrict, including applicable interoperability and security-research rights.
|
|
88
|
+
|
|
89
|
+
## 5. No warranty
|
|
90
|
+
|
|
91
|
+
**THE SOFTWARE IS PROVIDED "AS IS", WITH ALL FAULTS. TO THE FULLEST EXTENT THE
|
|
92
|
+
LAW ALLOWS, RUNBOUND GIVES NO WARRANTY, CONDITION OR GUARANTEE ABOUT IT,
|
|
93
|
+
WHETHER EXPRESS OR IMPLIED. RUNBOUND EXCLUDES THE IMPLIED WARRANTY OF
|
|
94
|
+
MERCHANTABILITY AND ANY IMPLIED WARRANTY OR CONDITION OF SATISFACTORY QUALITY,
|
|
95
|
+
FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT. RUNBOUND DOES NOT PROMISE
|
|
96
|
+
THAT THE SOFTWARE IS FREE OF ERRORS OR VULNERABILITIES, OR THAT IT WILL PREVENT
|
|
97
|
+
OR DETECT EVERY THREAT.**
|
|
98
|
+
|
|
99
|
+
## 6. Limitation of liability
|
|
100
|
+
|
|
101
|
+
**TO THE FULLEST EXTENT THE LAW ALLOWS, RUNBOUND IS NOT LIABLE, WHETHER IN
|
|
102
|
+
CONTRACT, TORT (INCLUDING NEGLIGENCE), UNDER STATUTE OR OTHERWISE, FOR ANY
|
|
103
|
+
INDIRECT, CONSEQUENTIAL, SPECIAL, INCIDENTAL OR PUNITIVE LOSS OR DAMAGES, OR
|
|
104
|
+
FOR ANY LOSS OF PROFIT, REVENUE, BUSINESS, GOODWILL OR ANTICIPATED SAVINGS,
|
|
105
|
+
LOSS OR CORRUPTION OF DATA, LOSS OF USE OF A SYSTEM, DOWNTIME OR BUSINESS
|
|
106
|
+
INTERRUPTION, OR THE COST OF RESTORING SYSTEMS OR DATA, ARISING OUT OF OR IN
|
|
107
|
+
CONNECTION WITH THE SOFTWARE OR THIS LICENCE, EVEN IF RUNBOUND WAS TOLD SUCH
|
|
108
|
+
LOSS WAS POSSIBLE. RUNBOUND'S TOTAL LIABILITY ARISING OUT OF OR IN CONNECTION
|
|
109
|
+
WITH THE SOFTWARE OR THIS LICENCE IS LIMITED TO GBP 100.**
|
|
110
|
+
|
|
111
|
+
This licence charges no fee for the Software, and these limits reflect that.
|
|
112
|
+
Sections 5 and 6 do not exclude or limit liability for death or personal
|
|
113
|
+
injury caused by negligence, fraud or fraudulent misrepresentation, wilful
|
|
114
|
+
misconduct or gross negligence, liability under mandatory product-liability
|
|
115
|
+
law, or any other liability that cannot lawfully be excluded or limited. They
|
|
116
|
+
do not reduce mandatory consumer rights. When you use the Software with a paid
|
|
117
|
+
Runbound service, the agreement for that service governs the service and
|
|
118
|
+
Runbound's liability for it, and sections 5 and 6 do not reduce the rights it
|
|
119
|
+
gives you.
|
|
120
|
+
|
|
121
|
+
## 7. Your applications and policies
|
|
122
|
+
|
|
123
|
+
You decide where to run the Software and which policies and configuration to
|
|
124
|
+
apply. An enforcing policy can block an operation, file access, process or
|
|
125
|
+
network connection in your application, and a configuration change can alter
|
|
126
|
+
how your application behaves. Either can stop a request, a job or the
|
|
127
|
+
application itself. A policy is enforced only on the runtimes, platforms and
|
|
128
|
+
modes that the documentation describes as supported. Elsewhere it may run
|
|
129
|
+
degraded or not apply.
|
|
130
|
+
|
|
131
|
+
Before you rely on the Software in production, test it, your policies and your
|
|
132
|
+
configuration outside production. Follow the documented rollout and rollback
|
|
133
|
+
steps, keep backups, and keep a way to operate and recover your application
|
|
134
|
+
that does not depend on Runbound. You are responsible for the policies and
|
|
135
|
+
configuration you choose, for checking compatibility with your applications
|
|
136
|
+
and for the effect of enforcement on them.
|
|
137
|
+
|
|
138
|
+
## 8. Duration and changes
|
|
139
|
+
|
|
140
|
+
The licence supplied with a release governs that release. Later terms do not
|
|
141
|
+
retroactively change it. Whether a standalone capability is free is determined
|
|
142
|
+
by the documentation supplied for that release; later documentation does not
|
|
143
|
+
withdraw that permission. The permissions for free standalone use and embedded
|
|
144
|
+
application distribution have no expiry while you comply with this licence.
|
|
145
|
+
Cloud access remains subject to the service agreement and entitlement. Ending
|
|
146
|
+
Cloud access does not end the permissions for free standalone capabilities.
|
|
147
|
+
|
|
148
|
+
If you materially breach this licence, Runbound may terminate your licence
|
|
149
|
+
only after written notice describing the breach and 30 days to remedy it,
|
|
150
|
+
unless it cannot be remedied. Termination does not remove mandatory legal
|
|
151
|
+
rights or the rights of recipients who remain in compliance.
|
|
152
|
+
|
|
153
|
+
This licence does not withdraw rights already validly granted under another
|
|
154
|
+
licence for an earlier copy, including an open-source licence. It grants no
|
|
155
|
+
rights in software or third-party contributions that Runbound cannot license.
|
|
156
|
+
|
|
157
|
+
## 9. Law
|
|
158
|
+
|
|
159
|
+
The law of England and Wales governs this licence. Consumers retain the
|
|
160
|
+
mandatory protections of the country where they live and may bring claims in
|
|
161
|
+
its courts. There is no mandatory arbitration or class-action waiver.
|
package/README.md
CHANGED
|
@@ -1,3 +1,137 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @runbound/config-browser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read typed configuration in a web page from one immutable snapshot, react to
|
|
4
|
+
changes through subscriptions, and pin a version for the length of a render or
|
|
5
|
+
user flow. It works with any framework, or none.
|
|
6
|
+
|
|
7
|
+
## About Runbound
|
|
8
|
+
|
|
9
|
+
Runbound runs workloads under policy and records what happened as evidence.
|
|
10
|
+
This package reads validated configuration in the browser.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @runbound/config-browser@alpha
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
It ships ES modules and TypeScript declarations for your application's bundler.
|
|
19
|
+
|
|
20
|
+
## Alpha limits
|
|
21
|
+
|
|
22
|
+
`0.1.0-alpha.1` is an early, unsupported alpha. APIs may change between alpha
|
|
23
|
+
releases.
|
|
24
|
+
|
|
25
|
+
- **Browsers and Node.js.** Tested in Chromium, Firefox and WebKit, and in
|
|
26
|
+
Node.js 22 and later, which is enough to try it without a page.
|
|
27
|
+
- **Cloud mode is not yet available.** Runbound Cloud does not yet deliver
|
|
28
|
+
managed Config, so the refresh controller has no origin to poll. Create the
|
|
29
|
+
client from a snapshot your application supplies, as below, and commit
|
|
30
|
+
updates you obtain yourself.
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
This runs entirely in your process and needs no account or network. Save it as
|
|
35
|
+
`first-value.mjs`:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { ConfigDeliveryClient } from "@runbound/config-browser";
|
|
39
|
+
|
|
40
|
+
const client = new ConfigDeliveryClient({
|
|
41
|
+
activation: { digest: "sha256:activation-1", sequence: 1 },
|
|
42
|
+
contextRevision: 1,
|
|
43
|
+
identity: {
|
|
44
|
+
clientId: "storefront-web",
|
|
45
|
+
contractDigest: "sha256:storefront-contract",
|
|
46
|
+
publicAudience: "storefront-public",
|
|
47
|
+
softwareReleaseId: "storefront-2026-10-19",
|
|
48
|
+
},
|
|
49
|
+
lifecycle: {
|
|
50
|
+
activeSource: "current",
|
|
51
|
+
compatibility: "compatible",
|
|
52
|
+
freshness: "fresh",
|
|
53
|
+
pendingLive: false,
|
|
54
|
+
readiness: "ready",
|
|
55
|
+
refresh: "idle",
|
|
56
|
+
restartRequired: false,
|
|
57
|
+
},
|
|
58
|
+
values: { checkout: { label: "Buy" } },
|
|
59
|
+
});
|
|
60
|
+
const label = (snapshot) => snapshot.values.checkout.label;
|
|
61
|
+
|
|
62
|
+
const session = client.openSession({
|
|
63
|
+
sessionId: "checkout-render-1",
|
|
64
|
+
openedAt: "2026-10-19T12:00:00.000Z",
|
|
65
|
+
expiresAt: "2026-10-19T12:05:00.000Z",
|
|
66
|
+
});
|
|
67
|
+
client.subscribeSelector(label, () => {
|
|
68
|
+
console.log(`label changed: ${label(client.getSnapshot())}`);
|
|
69
|
+
});
|
|
70
|
+
console.log(`label: ${label(client.getSnapshot())}`);
|
|
71
|
+
|
|
72
|
+
client.commit("activation", {
|
|
73
|
+
...client.getSnapshot(),
|
|
74
|
+
activation: { digest: "sha256:activation-2", sequence: 2 },
|
|
75
|
+
values: { checkout: { label: "Buy now" } },
|
|
76
|
+
});
|
|
77
|
+
console.log(`pinned session: ${session.read(label, "2026-10-19T12:01:00.000Z")}`);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
node first-value.mjs
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Expected output:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
label: Buy
|
|
88
|
+
label changed: Buy now
|
|
89
|
+
pinned session: Buy
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The subscriber runs only when its selected value changes, and the session
|
|
93
|
+
keeps the activation it opened with, so one render never mixes two versions.
|
|
94
|
+
|
|
95
|
+
## Read and update configuration
|
|
96
|
+
|
|
97
|
+
| API | Purpose |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `new ConfigDeliveryClient(snapshot)` | The store: one validated, immutable snapshot at a time. |
|
|
100
|
+
| `client.getSnapshot()`, `client.subscribe(listener)` | Read the current snapshot; hear about every commit. |
|
|
101
|
+
| `client.subscribeSelector(selector, listener, equality?)` | Hear only when a selected value changes. |
|
|
102
|
+
| `client.commit(kind, snapshot)` | Swap in a new `activation`, `context` or `status`; a commit that moves backwards or changes identity throws `ConfigDeliveryStoreError`. |
|
|
103
|
+
| `client.openSession({ sessionId, openedAt, expiresAt })` | Pin the current snapshot; `session.read(selector, now)` fails once it expires or closes. |
|
|
104
|
+
| `ConfigBrowserRefreshController` (`/config-refresh`) | Polls a managed delivery origin through a transport and verifier you supply. |
|
|
105
|
+
| `/config-context`, `/config-context-client` | Build and apply redacted evaluation-context revisions. |
|
|
106
|
+
| `/config-experimentation`, `/config-experiment-committed-render` | Record experiment assignments and release a variant only after its exposure is durable. |
|
|
107
|
+
|
|
108
|
+
The root entry exports all of these; the subpaths let a bundler load less.
|
|
109
|
+
|
|
110
|
+
## Network access
|
|
111
|
+
|
|
112
|
+
This package makes no requests of its own. The client and sessions work
|
|
113
|
+
entirely in memory. `ConfigBrowserRefreshController` calls only the `fetch`
|
|
114
|
+
of the transport object you pass it, with `credentials: "omit"`, and accepts
|
|
115
|
+
only Runbound's managed public origins, `https://config.runbound.io` and
|
|
116
|
+
`https://config.eu.runbound.io`, as its delivery origin; anything else fails
|
|
117
|
+
with `CONFIG_BROWSER_REFRESH_ORIGIN_INVALID` before any request. Responses
|
|
118
|
+
must carry your page's exact origin in `access-control-allow-origin`.
|
|
119
|
+
Experiment records leave the page only through the durable queue you supply.
|
|
120
|
+
|
|
121
|
+
## What it does not do
|
|
122
|
+
|
|
123
|
+
Everything a page holds is visible to its user: values, locally evaluated
|
|
124
|
+
rules and context. Treat them as presentation decisions and keep
|
|
125
|
+
authorization and sensitive rules on a trusted server. The client stores what
|
|
126
|
+
you commit; it is not signature verification, so verify remotely delivered
|
|
127
|
+
data before committing it. It keeps no disk cache and holds no credentials.
|
|
128
|
+
|
|
129
|
+
## Upgrade and uninstall
|
|
130
|
+
|
|
131
|
+
Pin the exact version; a page runs a new version when it next loads your
|
|
132
|
+
updated bundle. To uninstall, stop any refresh controller, close open
|
|
133
|
+
sessions, unsubscribe listeners and remove the package.
|
|
134
|
+
|
|
135
|
+
Source: [runbound-io/config](https://github.com/runbound-io/config/tree/main/sdks/browser).
|
|
136
|
+
Runbound software is proprietary under the Runbound Runtime and SDK Licence in this package's `LICENSE` file. The licence permits use in your own commercial and internal applications and distribution as an application dependency; it does not permit standalone redistribution. Cloud-required features need the appropriate Runbound service entitlement. Source is intended to be public for inspection and contributions through GitHub issues and pull requests; follow the repository contribution terms. Bundled third-party software is listed in
|
|
137
|
+
`THIRD_PARTY_NOTICES.md` and changes in `CHANGELOG.md`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
`@runbound/config-browser` bundles the third-party software below into its published JavaScript. Each entry gives the package, its licence, and the licence text, copyright and notices it is distributed under.
|
|
4
|
+
|
|
5
|
+
## ipaddr.js 2.4.0
|
|
6
|
+
|
|
7
|
+
- Licence: MIT
|
|
8
|
+
- Source: https://github.com/whitequark/ipaddr.js
|
|
9
|
+
|
|
10
|
+
~~~text
|
|
11
|
+
Copyright (C) 2011-2017 whitequark <whitequark@whitequark.org>
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in
|
|
21
|
+
all copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
29
|
+
THE SOFTWARE.
|
|
30
|
+
~~~
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type ConfigBrowserContextContract, type ConfigBrowserContextRevision, type ConfigBrowserContextScope } from "./config-context.js";
|
|
2
|
+
import { ConfigDeliveryClient, type ConfigDeliverySnapshot, type ConfigDeliveryValues } from "./config-delivery-store.js";
|
|
3
|
+
export type ConfigBrowserContextResolution<Values extends ConfigDeliveryValues> = Readonly<{
|
|
4
|
+
resolutionDigest: string;
|
|
5
|
+
values: Values;
|
|
6
|
+
}>;
|
|
7
|
+
export interface ConfigBrowserContextResolver<Values extends ConfigDeliveryValues> {
|
|
8
|
+
resolve(input: Readonly<{
|
|
9
|
+
activation: ConfigDeliverySnapshot<Values>["activation"];
|
|
10
|
+
context: ConfigBrowserContextRevision;
|
|
11
|
+
previousContext?: ConfigBrowserContextRevision;
|
|
12
|
+
}>): Promise<ConfigBrowserContextResolution<Values>>;
|
|
13
|
+
}
|
|
14
|
+
export type ConfigBrowserContextPartitionTransition = Readonly<{
|
|
15
|
+
fromIdentityGeneration?: number;
|
|
16
|
+
fromPartitionKey?: string;
|
|
17
|
+
reason: "identity-transition" | "scope-transition";
|
|
18
|
+
resolutionDigest: string;
|
|
19
|
+
toContextDigest: string;
|
|
20
|
+
toIdentityGeneration: number;
|
|
21
|
+
toPartitionKey: string;
|
|
22
|
+
toRevision: number;
|
|
23
|
+
}>;
|
|
24
|
+
export interface ConfigBrowserContextAtomicPartitionStore<Values extends ConfigDeliveryValues> {
|
|
25
|
+
/**
|
|
26
|
+
* Atomically seals the prior event/cache partition, activates the new
|
|
27
|
+
* partition, and invokes commit exactly once. Events retain their original
|
|
28
|
+
* partition provenance and are never relabelled or merged.
|
|
29
|
+
*/
|
|
30
|
+
commitTransition(transition: ConfigBrowserContextPartitionTransition, commit: () => ConfigDeliverySnapshot<Values>): Promise<Readonly<{
|
|
31
|
+
committed: true;
|
|
32
|
+
contextDigest: string;
|
|
33
|
+
partitionKey: string;
|
|
34
|
+
revision: number;
|
|
35
|
+
}>>;
|
|
36
|
+
}
|
|
37
|
+
export declare class ConfigBrowserContextClientError extends Error {
|
|
38
|
+
readonly code: string;
|
|
39
|
+
constructor(code: string, message: string);
|
|
40
|
+
}
|
|
41
|
+
export declare class ConfigBrowserContextClient<Values extends ConfigDeliveryValues> {
|
|
42
|
+
#private;
|
|
43
|
+
constructor(input: Readonly<{
|
|
44
|
+
client: ConfigDeliveryClient<Values>;
|
|
45
|
+
contract: ConfigBrowserContextContract;
|
|
46
|
+
initialContext?: ConfigBrowserContextRevision;
|
|
47
|
+
partitions: ConfigBrowserContextAtomicPartitionStore<Values>;
|
|
48
|
+
resolver: ConfigBrowserContextResolver<Values>;
|
|
49
|
+
}>);
|
|
50
|
+
get context(): ConfigBrowserContextRevision | undefined;
|
|
51
|
+
get diagnostic(): Readonly<{
|
|
52
|
+
contextContractDigest: string;
|
|
53
|
+
initialized: false;
|
|
54
|
+
valuesIncluded: false;
|
|
55
|
+
}> | {
|
|
56
|
+
contextContractDigest: string;
|
|
57
|
+
contextDigest: string;
|
|
58
|
+
identityGeneration: number;
|
|
59
|
+
keys: {
|
|
60
|
+
key: string;
|
|
61
|
+
sensitive: boolean;
|
|
62
|
+
source: "scope" | "trusted";
|
|
63
|
+
valueIncluded: false;
|
|
64
|
+
}[];
|
|
65
|
+
partitionKey: string;
|
|
66
|
+
revision: number;
|
|
67
|
+
subjectIdentityIncluded: false;
|
|
68
|
+
};
|
|
69
|
+
transition(input: Readonly<{
|
|
70
|
+
scopes: readonly ConfigBrowserContextScope[];
|
|
71
|
+
subjectIdentityToken: string;
|
|
72
|
+
trustedValues: Readonly<Record<string, boolean | number | string>>;
|
|
73
|
+
}>): Promise<ConfigDeliverySnapshot<Values>>;
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import*as t from"./config-context.js";import*as e from"./config-delivery-store.js";var i={50(e){e.exports=t},815(t){t.exports=e}},n={};function o(t){var e=n[t];if(void 0!==e)return e.exports;var r=n[t]={exports:{}};return i[t](r,r.exports,o),r.exports}o.d=(t,e,i)=>{var n=(e,i)=>{for(var n in e)o.o(e,n)&&!o.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,[i]:e[n]})};n(e,"get"),n(i,"value")},o.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var r={},s=o(50),a=o(815);class c extends Error{code;constructor(t,e){super(e),this.code=t,this.name="ConfigBrowserContextClientError"}}class l{#t;#e;#i;#n;#o;#r=0;constructor(t){this.#t=t.client,this.#e=structuredClone(t.contract),this.#i=t.partitions,this.#n=t.resolver,t.initialContext?((t.initialContext.contextContractDigest!==t.contract.contextContractDigest||t.initialContext.revision!==t.client.getSnapshot().contextRevision)&&x("CONFIG_BROWSER_CONTEXT_INITIAL_MISMATCH","Initial context does not match the delivery client and contract."),this.#o=t.initialContext):0!==t.client.getSnapshot().contextRevision&&x("CONFIG_BROWSER_CONTEXT_INITIAL_REQUIRED","A client with an existing context revision requires its exact context.")}get context(){return this.#o}get diagnostic(){return this.#o?(0,s.configBrowserContextRedactedDiagnostic)(this.#o):Object.freeze({contextContractDigest:this.#e.contextContractDigest,initialized:!1,valuesIncluded:!1})}async transition(t){var e;this.#r+=1;let i=this.#r,n=this.#o,o=this.#t.getSnapshot(),r=(0,s.createConfigBrowserContextRevision)({contract:this.#e,...n?{previous:n}:{},scopes:t.scopes,subjectIdentityToken:t.subjectIdentityToken,trustedValues:t.trustedValues}),c=await this.#n.resolve({activation:o.activation,context:r,...n?{previousContext:n}:{}});e=c.resolutionDigest,/^sha256:[0-9a-f]{64}$/u.test(e)||x("CONFIG_BROWSER_CONTEXT_RESOLUTION_INVALID","resolutionDigest is invalid."),i!==this.#r&&x("CONFIG_BROWSER_CONTEXT_TRANSITION_SUPERSEDED","A newer context transition superseded this resolution.");let l={...o,contextRevision:r.revision,values:c.values};new a.ConfigDeliveryClient(o).commit("context",l);let C=0,h=await this.#i.commitTransition({...n?{fromIdentityGeneration:n.identityGeneration,fromPartitionKey:n.partitionKey}:{},reason:n?.identityGeneration===r.identityGeneration?"scope-transition":"identity-transition",resolutionDigest:c.resolutionDigest,toContextDigest:r.contextDigest,toIdentityGeneration:r.identityGeneration,toPartitionKey:r.partitionKey,toRevision:r.revision},()=>{1!==(C+=1)&&x("CONFIG_BROWSER_CONTEXT_COMMIT_DUPLICATE","Context partition transaction invoked its commit more than once."),(i!==this.#r||this.#t.getSnapshot()!==o||this.#o!==n)&&x("CONFIG_BROWSER_CONTEXT_TRANSITION_CONFLICT","Context or activation changed during the atomic transition.");let t=this.#t.commit("context",l);return this.#o=r,t});return(1!==C||!0!==h.committed||h.contextDigest!==r.contextDigest||h.partitionKey!==r.partitionKey||h.revision!==r.revision)&&x("CONFIG_BROWSER_CONTEXT_PARTITION_COMMIT_INVALID","Context partition transaction did not acknowledge the exact revision."),this.#t.getSnapshot()}}function x(t,e){throw new c(t,e)}o.d(r,{X:()=>l,n:()=>c});var C=r.X,h=r.n;export{C as ConfigBrowserContextClient,h as ConfigBrowserContextClientError};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type ConfigBrowserContextContract = Readonly<{
|
|
2
|
+
attributes: readonly Readonly<{
|
|
3
|
+
clientWritable: boolean;
|
|
4
|
+
key: string;
|
|
5
|
+
maximumLength?: number;
|
|
6
|
+
sensitive: boolean;
|
|
7
|
+
type: "boolean" | "number" | "string";
|
|
8
|
+
}>[];
|
|
9
|
+
contextContractDigest: string;
|
|
10
|
+
trustedAttributeKeys: readonly string[];
|
|
11
|
+
}>;
|
|
12
|
+
export type ConfigBrowserContextScope = Readonly<{
|
|
13
|
+
scopeId: string;
|
|
14
|
+
values: Readonly<Record<string, boolean | number | string>>;
|
|
15
|
+
}>;
|
|
16
|
+
export type ConfigBrowserContextRevision = Readonly<{
|
|
17
|
+
contextContractDigest: string;
|
|
18
|
+
contextDigest: string;
|
|
19
|
+
explanation: readonly Readonly<{
|
|
20
|
+
key: string;
|
|
21
|
+
sensitive: boolean;
|
|
22
|
+
source: "scope" | "trusted";
|
|
23
|
+
}>[];
|
|
24
|
+
identityGeneration: number;
|
|
25
|
+
partitionKey: string;
|
|
26
|
+
revision: number;
|
|
27
|
+
scopeIds: readonly string[];
|
|
28
|
+
subjectIdentityTokenDigest: string;
|
|
29
|
+
values: Readonly<Record<string, boolean | number | string>>;
|
|
30
|
+
}>;
|
|
31
|
+
export declare class ConfigBrowserContextError extends Error {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
constructor(code: string, message: string);
|
|
34
|
+
}
|
|
35
|
+
export declare function createConfigBrowserContextRevision(input: {
|
|
36
|
+
contract: ConfigBrowserContextContract;
|
|
37
|
+
previous?: ConfigBrowserContextRevision;
|
|
38
|
+
scopes: readonly ConfigBrowserContextScope[];
|
|
39
|
+
subjectIdentityToken: string;
|
|
40
|
+
trustedValues: Readonly<Record<string, boolean | number | string>>;
|
|
41
|
+
}): ConfigBrowserContextRevision;
|
|
42
|
+
export declare function configBrowserContextRedactedDiagnostic(revision: ConfigBrowserContextRevision): {
|
|
43
|
+
contextContractDigest: string;
|
|
44
|
+
contextDigest: string;
|
|
45
|
+
identityGeneration: number;
|
|
46
|
+
keys: {
|
|
47
|
+
key: string;
|
|
48
|
+
sensitive: boolean;
|
|
49
|
+
source: "scope" | "trusted";
|
|
50
|
+
valueIncluded: false;
|
|
51
|
+
}[];
|
|
52
|
+
partitionKey: string;
|
|
53
|
+
revision: number;
|
|
54
|
+
subjectIdentityIncluded: false;
|
|
55
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import*as t from"@noble/hashes/sha2.js";import*as e from"@noble/hashes/utils.js";var n={961(e){e.exports=t},334(t){t.exports=e}},o={};function r(t){var e=o[t];if(void 0!==e)return e.exports;var s=o[t]={exports:{}};return n[t](s,s.exports,r),s.exports}r.d=(t,e,n)=>{var o=(e,n)=>{for(var o in e)r.o(e,o)&&!r.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,[n]:e[o]})};o(e,"get"),o(n,"value")},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var s={},i=r(961),a=r(334);class c extends Error{code;constructor(t,e){super(e),this.code=t,this.name="ConfigBrowserContextError"}}function u(t){let e=function(t){var e;e=t.contextContractDigest,/^sha256:[0-9a-f]{64}$/u.test(e)||d("CONFIG_BROWSER_CONTEXT_INPUT_INVALID","contextContractDigest is not a digest."),(0===t.attributes.length||t.attributes.length>256)&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Context contract requires one to 256 attributes.");let n=new Set;for(let e of t.attributes)O(e.key,"attribute.key",256),n.has(e.key)&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Context attribute keys must be unique."),n.add(e.key),void 0!==e.maximumLength&&(!Number.isSafeInteger(e.maximumLength)||e.maximumLength<1||e.maximumLength>4096)&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Context string bounds are invalid.");return(new Set(t.trustedAttributeKeys).size!==t.trustedAttributeKeys.length||t.trustedAttributeKeys.some(t=>!n.has(t)))&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Trusted context keys must be unique contract attributes."),l(structuredClone(t))}(t.contract);t.previous&&t.previous.contextContractDigest!==e.contextContractDigest&&d("CONFIG_BROWSER_CONTEXT_CONTRACT_CHANGED","Context revisions cannot cross generated contract identity.");let n=p(O(t.subjectIdentityToken,"subjectIdentityToken",2048)),o=t.previous?.subjectIdentityTokenDigest===n?t.previous.identityGeneration:(t.previous?.identityGeneration??0)+1,r=new Map(e.attributes.map(t=>[t.key,t])),s=new Set(e.trustedAttributeKeys),i={},a=new Map,c=new Set;for(let e of(t.scopes.length>64&&d("CONFIG_BROWSER_CONTEXT_SCOPE_LIMIT","Context scope limit exceeded."),t.scopes))for(let[t,n]of(O(e.scopeId,"scopeId",256),c.has(e.scopeId)&&d("CONFIG_BROWSER_CONTEXT_SCOPE_DUPLICATE","Context scope IDs must be unique."),c.add(e.scopeId),Object.entries(e.values))){let e=r.get(t);(!e?.clientWritable||s.has(t))&&d("CONFIG_BROWSER_CONTEXT_SCOPE_FORBIDDEN","A context scope attempted to set an unknown or trusted attribute."),_(e,n),i[t]=n,a.set(t,"scope")}let u=Object.keys(t.trustedValues).sort();for(let e of((u.length!==s.size||u.some(t=>!s.has(t)))&&d("CONFIG_BROWSER_CONTEXT_TRUST_INCOMPLETE","Trusted context must provide every reserved attribute exactly once."),u)){let n=r.get(e);n||d("CONFIG_BROWSER_CONTEXT_CONTRACT_INVALID","Trusted context key is absent from its contract.");let o=t.trustedValues[e];_(n,o),i[e]=o,a.set(e,"trusted")}let C=Object.fromEntries(Object.entries(i).sort(([t],[e])=>t.localeCompare(e))),T=p({contextContractDigest:e.contextContractDigest,identityGeneration:o,scopeIds:[...c],subjectIdentityTokenDigest:n,values:C}),x=(t.previous?.revision??0)+1;return l({contextContractDigest:e.contextContractDigest,contextDigest:T,explanation:Object.keys(C).map(t=>{let e=r.get(t),n=a.get(t);return e&&n||d("CONFIG_BROWSER_CONTEXT_STATE_INVALID","Context explanation lost its attribute lineage."),{key:t,sensitive:e.sensitive,source:n}}),identityGeneration:o,partitionKey:p({contextContractDigest:e.contextContractDigest,identityGeneration:o,subjectIdentityTokenDigest:n}),revision:x,scopeIds:[...c],subjectIdentityTokenDigest:n,values:C})}function C(t){return l({contextContractDigest:t.contextContractDigest,contextDigest:t.contextDigest,identityGeneration:t.identityGeneration,keys:t.explanation.map(({key:t,sensitive:e,source:n})=>({key:t,sensitive:e,source:n,valueIncluded:!1})),partitionKey:t.partitionKey,revision:t.revision,subjectIdentityIncluded:!1})}function _(t,e){(typeof e!==t.type||"number"==typeof e&&!Number.isFinite(e)||"string"==typeof e&&e.length>(t.maximumLength??4096))&&d("CONFIG_BROWSER_CONTEXT_VALUE_INVALID",`Context attribute ${t.key} has an invalid value.`)}function O(t,e,n){return("string"!=typeof t||t.trim()!==t||0===t.length||t.length>n)&&d("CONFIG_BROWSER_CONTEXT_INPUT_INVALID",`${e} is invalid.`),t}function p(t){let e=new TextEncoder().encode(function t(e){return null===e||"boolean"==typeof e||"string"==typeof e||"number"==typeof e&&Number.isFinite(e)?JSON.stringify(e):Array.isArray(e)?`[${e.map(e=>t(e)).join(",")}]`:e&&"object"==typeof e?`{${Object.entries(e).sort(([t],[e])=>t.localeCompare(e)).map(([e,n])=>`${JSON.stringify(e)}:${t(n)}`).join(",")}}`:void d("CONFIG_BROWSER_CONTEXT_STATE_INVALID","Context digest input is not canonical JSON.")}(t));return`sha256:${(0,a.bytesToHex)((0,i.sha256)(e))}`}function d(t,e){throw new c(t,e)}function l(t){if(t&&"object"==typeof t&&!Object.isFrozen(t))for(let e of(Object.freeze(t),Object.values(t)))l(e);return t}r.d(s,{HR:()=>u,_B:()=>c,xn:()=>C});var T=s._B,x=s.xn,N=s.HR;export{T as ConfigBrowserContextError,x as configBrowserContextRedactedDiagnostic,N as createConfigBrowserContextRevision};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { JsonValue } from "./internal/config-contract/core.js";
|
|
2
|
+
export type ConfigDeliveryValues = Readonly<Record<string, JsonValue>>;
|
|
3
|
+
export type ConfigDeliveryClientIdentity = Readonly<{
|
|
4
|
+
clientId: string;
|
|
5
|
+
contractDigest: string;
|
|
6
|
+
publicAudience: string;
|
|
7
|
+
softwareReleaseId: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type ConfigDeliveryLifecycle = Readonly<{
|
|
10
|
+
activeSource: "current" | "defaults" | "lastKnownGood" | "pinnedOffline";
|
|
11
|
+
compatibility: "compatible" | "incompatible";
|
|
12
|
+
freshness: "expired" | "fresh" | "stale";
|
|
13
|
+
pendingLive: boolean;
|
|
14
|
+
readiness: "error" | "initializing" | "ready";
|
|
15
|
+
reason?: Readonly<{
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}>;
|
|
19
|
+
refresh: "failed" | "idle" | "refreshing";
|
|
20
|
+
restartRequired: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
export type ConfigDeliverySnapshot<Values extends ConfigDeliveryValues> = Readonly<{
|
|
23
|
+
activation: Readonly<{
|
|
24
|
+
digest: string;
|
|
25
|
+
sequence: number;
|
|
26
|
+
}>;
|
|
27
|
+
contextRevision: number;
|
|
28
|
+
identity: ConfigDeliveryClientIdentity;
|
|
29
|
+
lifecycle: ConfigDeliveryLifecycle;
|
|
30
|
+
values: Values;
|
|
31
|
+
}>;
|
|
32
|
+
export type ConfigDeliveryCommitKind = "activation" | "context" | "status";
|
|
33
|
+
export type ConfigDeliveryEquality<Selected> = (left: Selected, right: Selected) => boolean;
|
|
34
|
+
export type ConfigDeliverySelector<Values extends ConfigDeliveryValues, Selected> = (snapshot: ConfigDeliverySnapshot<Values>) => Selected;
|
|
35
|
+
export declare class ConfigDeliveryStoreError extends Error {
|
|
36
|
+
readonly code: string;
|
|
37
|
+
constructor(code: string, message: string);
|
|
38
|
+
}
|
|
39
|
+
export declare class ConfigDeliveryPinnedSession<Values extends ConfigDeliveryValues = ConfigDeliveryValues> {
|
|
40
|
+
#private;
|
|
41
|
+
readonly clientId: string;
|
|
42
|
+
readonly expiresAt: string;
|
|
43
|
+
readonly openedAt: string;
|
|
44
|
+
readonly sessionId: string;
|
|
45
|
+
readonly snapshot: ConfigDeliverySnapshot<Values>;
|
|
46
|
+
constructor(input: {
|
|
47
|
+
expiresAt: string;
|
|
48
|
+
openedAt: string;
|
|
49
|
+
sessionId: string;
|
|
50
|
+
snapshot: ConfigDeliverySnapshot<Values>;
|
|
51
|
+
});
|
|
52
|
+
close(): void;
|
|
53
|
+
read<Selected>(selector: ConfigDeliverySelector<Values, Selected>, now?: string): Selected;
|
|
54
|
+
assertUsable(now: string): void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Framework-neutral public Config store. Candidates are normalized off the
|
|
58
|
+
* read path and become visible through one immutable pointer swap.
|
|
59
|
+
*/
|
|
60
|
+
export declare class ConfigDeliveryClient<Values extends ConfigDeliveryValues = ConfigDeliveryValues> {
|
|
61
|
+
#private;
|
|
62
|
+
constructor(initialSnapshot: ConfigDeliverySnapshot<Values>);
|
|
63
|
+
get identity(): ConfigDeliveryClientIdentity;
|
|
64
|
+
getSnapshot: () => ConfigDeliverySnapshot<Values>;
|
|
65
|
+
subscribe: (listener: () => void) => (() => void);
|
|
66
|
+
subscribeSelector<Selected>(selector: ConfigDeliverySelector<Values, Selected>, listener: () => void, equality?: ConfigDeliveryEquality<Selected>): () => void;
|
|
67
|
+
commit(kind: ConfigDeliveryCommitKind, candidate: ConfigDeliverySnapshot<Values>): ConfigDeliverySnapshot<Values>;
|
|
68
|
+
openSession(input: {
|
|
69
|
+
expiresAt: string;
|
|
70
|
+
openedAt: string;
|
|
71
|
+
sessionId: string;
|
|
72
|
+
}): ConfigDeliveryPinnedSession<Values>;
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e={};e.d=(t,n,i)=>{var s=(n,i)=>{for(var s in n)e.o(n,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,[i]:n[s]})};s(n,"get"),s(i,"value")},e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var t={};class n extends Error{code;constructor(e,t){super(t),this.code=e,this.name="ConfigDeliveryStoreError"}}class i{clientId;expiresAt;openedAt;sessionId;snapshot;#e=!1;constructor(e){if(d(e.openedAt,"openedAt"),d(e.expiresAt,"expiresAt"),Date.parse(e.expiresAt)<=Date.parse(e.openedAt))throw new n("CONFIG_DELIVERY_SESSION_EXPIRY_INVALID","A pinned Config render session must expire after it opens.");u(e.sessionId,"sessionId"),this.clientId=e.snapshot.identity.clientId,this.expiresAt=e.expiresAt,this.openedAt=e.openedAt,this.sessionId=e.sessionId,this.snapshot=e.snapshot}close(){this.#e=!0}read(e,t=new Date().toISOString()){return this.assertUsable(t),e(this.snapshot)}assertUsable(e){if(d(e,"now"),this.#e)throw new n("CONFIG_DELIVERY_SESSION_CLOSED","The pinned Config render session is closed.");if(Date.parse(e)>=Date.parse(this.expiresAt))throw new n("CONFIG_DELIVERY_SESSION_EXPIRED","The pinned Config render session exceeded its declared maximum age.")}}class s{#t=new Set;#n;constructor(e){this.#n=o(void 0,e)}get identity(){return this.#n.identity}getSnapshot=()=>this.#n;subscribe=e=>(this.#t.add(e),()=>this.#t.delete(e));subscribeSelector(e,t,n=Object.is){let i=e(this.#n);return this.subscribe(()=>{let s=e(this.#n);n(i,s)||(i=s,t())})}commit(e,t){!function(e,t,i){var s,o;if(s=t.identity,o=i.identity,s.clientId!==o.clientId||s.contractDigest!==o.contractDigest||s.publicAudience!==o.publicAudience||s.softwareReleaseId!==o.softwareReleaseId)throw new n("CONFIG_DELIVERY_CLIENT_IDENTITY_CHANGED","A Config client cannot change contract, audience, software, or client identity.");if("activation"===e){if(i.activation.sequence<=t.activation.sequence||i.contextRevision!==t.contextRevision)throw new n("CONFIG_DELIVERY_ACTIVATION_SEQUENCE_INVALID","An activation commit must advance the sequence without changing context revision.");return}if(i.activation.sequence!==t.activation.sequence||i.activation.digest!==t.activation.digest)throw new n("CONFIG_DELIVERY_PINNED_ACTIVATION_CHANGED","Context and status commits cannot replace the active Config activation.");if("context"===e){if(i.contextRevision<=t.contextRevision)throw new n("CONFIG_DELIVERY_CONTEXT_REVISION_INVALID","A context commit must advance its revision atomically.");return}if(i.contextRevision!==t.contextRevision)throw new n("CONFIG_DELIVERY_STATUS_CONTEXT_CHANGED","A lifecycle-only commit cannot change context revision.");if(c(t.values,i.values)!==t.values)throw new n("CONFIG_DELIVERY_STATUS_VALUES_CHANGED","A lifecycle-only commit cannot change Config values.")}(e,this.#n,t);let i=o(this.#n,t);for(let e of(this.#n=i,[...this.#t]))e();return i}openSession(e){return new i({...e,snapshot:this.#n})}}function o(e,t){(function(e,t){if(!Number.isSafeInteger(e)||e<1)throw new n("CONFIG_DELIVERY_INPUT_INVALID",`${t} must be a positive safe integer.`)})(t.activation.sequence,"activation.sequence"),u(t.activation.digest,"activation.digest"),function(e,t){if(!Number.isSafeInteger(e)||e<0)throw new n("CONFIG_DELIVERY_INPUT_INVALID",`${t} must be a non-negative safe integer.`)}(t.contextRevision,"contextRevision"),function(e){for(let[t,n]of Object.entries(e))u(n,`identity.${t}`)}(t.identity),function(e){if(!["current","defaults","lastKnownGood","pinnedOffline"].includes(e.activeSource)||!["compatible","incompatible"].includes(e.compatibility)||!["expired","fresh","stale"].includes(e.freshness)||!["error","initializing","ready"].includes(e.readiness)||!["failed","idle","refreshing"].includes(e.refresh)||"boolean"!=typeof e.pendingLive||"boolean"!=typeof e.restartRequired)throw new n("CONFIG_DELIVERY_LIFECYCLE_INVALID","Config lifecycle state must use the closed readiness, source, freshness, refresh, compatibility, and restart contract.");e.reason&&(u(e.reason.code,"lifecycle.reason.code"),u(e.reason.message,"lifecycle.reason.message"))}(t.lifecycle);let i=c(e?.values,t.values);return Object.freeze({activation:r(e?.activation,t.activation),contextRevision:t.contextRevision,identity:r(e?.identity,t.identity),lifecycle:function(e,t){if(!e)return f(t);let{reason:n,...i}=e,{reason:s,...o}=t;return(void 0===n||void 0===s?n===s:a(n,s))&&"reason"in e=="reason"in t&&a(i,o)?e:f(t)}(e?.lifecycle,t.lifecycle),values:i})}function r(e,t){return e&&a(e,t)?e:Object.freeze({...t})}function a(e,t){let n=Object.keys(t);if(Object.keys(e).length!==n.length)return!1;for(let i of n)if(!Object.hasOwn(e,i)||!Object.is(e[i],t[i]))return!1;return!0}function c(e,t){if(null===t||"boolean"==typeof t||"string"==typeof t)return Object.is(e,t)?e:t;if("number"==typeof t){if(!Number.isFinite(t))throw l();return Object.is(e,t)?e:t}if(Array.isArray(t)){let n=Array.isArray(e)?e:[],i=t.map((e,t)=>c(n[t],e));return Array.isArray(e)&&e.length===i.length&&i.every((t,n)=>t===e[n])?e:Object.freeze(i)}if(t&&"object"==typeof t){let n=Object.getPrototypeOf(t);if(n!==Object.prototype&&null!==n)throw l();let i=e&&"object"==typeof e&&!Array.isArray(e)?e:{},s=Object.keys(t).sort(),o=Object.fromEntries(s.map(e=>[e,c(i[e],t[e])]));return Object.keys(i).length===s.length&&s.every(e=>o[e]===i[e])?e:Object.freeze(o)}throw l()}function l(){return new n("CONFIG_DELIVERY_VALUES_INVALID","Public Config snapshots must contain only finite JSON values.")}function f(e){return Object.freeze({...e,...e.reason?{reason:Object.freeze({...e.reason})}:{}})}function u(e,t){if("string"!=typeof e||0===e.trim().length||e.length>1024)throw new n("CONFIG_DELIVERY_INPUT_INVALID",`${t} must be a bounded non-empty value.`)}function d(e,t){if(!Number.isFinite(Date.parse(e)))throw new n("CONFIG_DELIVERY_INPUT_INVALID",`${t} must be an ISO timestamp.`)}e.d(t,{D6:()=>i,Kv:()=>n,cH:()=>s});var h=t.cH,I=t.D6,p=t.Kv;export{h as ConfigDeliveryClient,I as ConfigDeliveryPinnedSession,p as ConfigDeliveryStoreError};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BrowserExperimentAssignmentReceipt } from "./config-experimentation.js";
|
|
2
|
+
export type BrowserExperimentRenderKind = "client" | "fallback" | "ssr";
|
|
3
|
+
export type BrowserExperimentCommittedRenderCandidate = Readonly<{
|
|
4
|
+
assignment: BrowserExperimentAssignmentReceipt;
|
|
5
|
+
deduplicationKey: string;
|
|
6
|
+
exposureBoundaryId: string;
|
|
7
|
+
exclusionReason?: "fallback-render" | "server-render";
|
|
8
|
+
renderAttemptId: string;
|
|
9
|
+
renderKind: BrowserExperimentRenderKind;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
}>;
|
|
12
|
+
export interface BrowserExperimentCommittedRenderStore {
|
|
13
|
+
claim(deduplicationKey: string): Promise<"claimed" | "duplicate">;
|
|
14
|
+
complete(deduplicationKey: string): Promise<void>;
|
|
15
|
+
release(deduplicationKey: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export type BrowserExperimentCommittedRenderResult<Result> = Readonly<{
|
|
18
|
+
reason: "abandoned-render" | "fallback-render" | "server-render";
|
|
19
|
+
status: "excluded";
|
|
20
|
+
}> | Readonly<{
|
|
21
|
+
status: "duplicate";
|
|
22
|
+
}> | Readonly<{
|
|
23
|
+
exposure: Result;
|
|
24
|
+
status: "exposed";
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Framework-neutral committed-render boundary. React adapters may prepare
|
|
28
|
+
* during render, abandon from discarded work, and commit from the post-commit
|
|
29
|
+
* lifecycle. The injected store owns cross-instance StrictMode/remount dedupe.
|
|
30
|
+
*
|
|
31
|
+
* Attempt state is keyed weakly by the candidate object, and only live
|
|
32
|
+
* (prepared or committing) attempt identifiers are retained. Settled,
|
|
33
|
+
* excluded and abandoned attempts keep their answers for whoever still holds
|
|
34
|
+
* the candidate, but the controller itself no longer keeps candidates or
|
|
35
|
+
* assignments reachable after their render lifecycle ends; a strong per-attempt
|
|
36
|
+
* map previously grew with every mount for the controller's lifetime.
|
|
37
|
+
*/
|
|
38
|
+
export declare class BrowserExperimentCommittedRenderController {
|
|
39
|
+
#private;
|
|
40
|
+
constructor(store: BrowserExperimentCommittedRenderStore);
|
|
41
|
+
prepare(input: {
|
|
42
|
+
assignment: BrowserExperimentAssignmentReceipt;
|
|
43
|
+
exposureBoundaryId: string;
|
|
44
|
+
renderAttemptId: string;
|
|
45
|
+
renderKind: BrowserExperimentRenderKind;
|
|
46
|
+
sessionId: string;
|
|
47
|
+
}): BrowserExperimentCommittedRenderCandidate;
|
|
48
|
+
abandon(candidate: BrowserExperimentCommittedRenderCandidate): void;
|
|
49
|
+
commit<Result>(candidate: BrowserExperimentCommittedRenderCandidate, expose: (assignment: BrowserExperimentAssignmentReceipt, exposureBoundaryId: string) => Promise<Result>): Promise<BrowserExperimentCommittedRenderResult<Result>>;
|
|
50
|
+
}
|
|
51
|
+
/** Test/reference store only; browser adapters inject persistent storage. */
|
|
52
|
+
export declare class MemoryBrowserExperimentCommittedRenderStore implements BrowserExperimentCommittedRenderStore {
|
|
53
|
+
#private;
|
|
54
|
+
claim(deduplicationKey: string): Promise<"claimed" | "duplicate">;
|
|
55
|
+
complete(deduplicationKey: string): Promise<void>;
|
|
56
|
+
release(deduplicationKey: string): Promise<void>;
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e={};e.d=(t,r,s)=>{var i=(r,s)=>{for(var i in r)e.o(r,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,[s]:r[i]})};i(r,"get"),i(s,"value")},e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var t={};class r{#e=new Set;#t=new WeakMap;#r=new WeakSet;#s;constructor(e){this.#s=e}prepare(e){if(i(e.exposureBoundaryId,"exposureBoundaryId"),i(e.renderAttemptId,"renderAttemptId"),i(e.sessionId,"sessionId"),this.#e.has(e.renderAttemptId))throw Error("A render attempt identifier cannot be prepared twice.");if(e.assignment.record.sessionId!==e.sessionId)throw Error("Committed-render exposure must remain in the durable assignment session.");let t="ssr"===e.renderKind?"server-render":"fallback"===e.renderKind?"fallback-render":void 0,r=Object.freeze({assignment:e.assignment,deduplicationKey:JSON.stringify([e.sessionId,e.assignment.semanticIdentity.runId,e.assignment.exposureToken,e.exposureBoundaryId]),exposureBoundaryId:e.exposureBoundaryId,...t?{exclusionReason:t}:{},renderAttemptId:e.renderAttemptId,renderKind:e.renderKind,sessionId:e.sessionId});return this.#t.set(r,"prepared"),this.#e.add(e.renderAttemptId),r}abandon(e){let t=this.#i(e);"committing"===t?this.#r.add(e):"settled"!==t&&"abandoned"!==t&&this.#n(e,"abandoned")}async commit(e,t){let r=this.#i(e);if(e.exclusionReason)return"prepared"===r&&this.#n(e,"settled"),Object.freeze({reason:e.exclusionReason,status:"excluded"});if("abandoned"===r)return Object.freeze({reason:"abandoned-render",status:"excluded"});if("committing"===r||"settled"===r)return Object.freeze({status:"duplicate"});this.#t.set(e,"committing");let s=!1,i=!1;try{let r=await this.#s.claim(e.deduplicationKey);if("duplicate"===r)return this.#n(e,"settled"),Object.freeze({status:"duplicate"});s=!0;let n=await t(e.assignment,e.exposureBoundaryId);return i=!0,await this.#s.complete(e.deduplicationKey),this.#n(e,"settled"),Object.freeze({exposure:n,status:"exposed"})}catch(t){if(i)throw this.#n(e,"settled"),t;try{s&&await this.#s.release(e.deduplicationKey)}finally{this.#r.has(e)?this.#n(e,"abandoned"):this.#t.set(e,"prepared")}throw t}}#n(e,t){this.#t.set(e,t),this.#r.delete(e),this.#e.delete(e.renderAttemptId)}#i(e){let t=this.#t.get(e);if(!t)throw Error("Committed-render candidates are valid only for their preparing controller.");return t}}class s{#d=new Map;async claim(e){return this.#d.has(e)?"duplicate":(this.#d.set(e,"claimed"),"claimed")}async complete(e){if("claimed"!==this.#d.get(e))throw Error("A committed-render claim must exist before completion.");this.#d.set(e,"completed")}async release(e){"claimed"===this.#d.get(e)&&this.#d.delete(e)}}function i(e,t){if("string"!=typeof e||0===e.trim().length||e.length>512)throw Error(`${t} must be a bounded non-empty identifier.`)}e.d(t,{e:()=>s,o:()=>r});var n=t.o,d=t.e;export{n as BrowserExperimentCommittedRenderController,d as MemoryBrowserExperimentCommittedRenderStore};
|