cardano-tessera-respond 0.1.0
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/LICENSE +201 -0
- package/README.md +456 -0
- package/dist/element-BvO1mbzk.js +3175 -0
- package/dist/index-Bs4cDxvQ.js +18375 -0
- package/dist/index-CN6gqkFj.js +4344 -0
- package/dist/index-UG4xdGya.js +29 -0
- package/dist/tessera-respond.es.d.ts +586 -0
- package/dist/tessera-respond.es.js +4 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or Derivative
|
|
95
|
+
Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work, excluding
|
|
103
|
+
those notices that do not pertain to any part of the Derivative
|
|
104
|
+
Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and do
|
|
117
|
+
not modify the License. You may add Your own attribution notices
|
|
118
|
+
within Derivative Works that You distribute, alongside or as an
|
|
119
|
+
addendum to the NOTICE text from the Work, provided that such
|
|
120
|
+
additional attribution notices cannot be construed as modifying
|
|
121
|
+
the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Matthieu Pizenberg and Tessera contributors
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# cardano-tessera-respond
|
|
2
|
+
|
|
3
|
+
The embeddable **`<tessera-respond>`** custom element: drop it into any web page to
|
|
4
|
+
let a user answer a [CIP-179](../../frontend/cip-179.md) survey. Give it a survey
|
|
5
|
+
definition and a responder (a role→credential map) as props; it renders the answering UI,
|
|
6
|
+
validates the answers, timelock-encrypts them for a sealed survey, and emits a
|
|
7
|
+
**ready-to-attach label-17 `Metadatum`** back to you via a `CustomEvent`. You
|
|
8
|
+
attach that payload to a transaction and submit it.
|
|
9
|
+
|
|
10
|
+
Framework-agnostic (it's a Web Component — use it from React, Vue, Svelte, Solid,
|
|
11
|
+
or plain HTML). **The widget never touches a wallet, a chain, IPFS,
|
|
12
|
+
`localStorage`, or your page's `<html>`.** Connecting a wallet, reading the chain,
|
|
13
|
+
building/signing/submitting the transaction, and network conformance are all
|
|
14
|
+
yours.
|
|
15
|
+
|
|
16
|
+
## Division of labor
|
|
17
|
+
|
|
18
|
+
| Concern | Owner |
|
|
19
|
+
| :------------------------------------------------------------------- | :------------------------------------------------ |
|
|
20
|
+
| Wallet connection (CIP-30/95), signing, submission | **Host** |
|
|
21
|
+
| Network conformance (wallet network ↔ target chain) | **Host** — the widget never sees a network id |
|
|
22
|
+
| Fetching / enriching the `SurveyDefinition` | **Host** |
|
|
23
|
+
| Supplying the chain tip (`tipEpoch`) and cancellation (`cancelled`) | **Host** (props, refreshed as it sees fit) |
|
|
24
|
+
| IPFS pinning for a rationale | **Host** (passes a pre-pinned anchor) |
|
|
25
|
+
| Rendering the form, validation, sealed encryption, label-17 encoding | **Widget** |
|
|
26
|
+
| Open/closed gating from the tip; quicknet check for a sealed survey | **Widget** (`surveyStatus` from `cip-179/domain`) |
|
|
27
|
+
|
|
28
|
+
The widget hands you a validated (and, if sealed, already-encrypted) payload. You
|
|
29
|
+
are responsible only for attaching it at metadata label 17, **proving** each
|
|
30
|
+
declared credential through the carrying transaction (see
|
|
31
|
+
[Proving credentials](#proving-credentials)), signing, and submitting.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm install cardano-tessera-respond
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import "cardano-tessera-respond"; // registers <tessera-respond>
|
|
41
|
+
import type { RespondResult } from "cardano-tessera-respond";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The published package is the self-contained built artifact — Solid,
|
|
45
|
+
`respond-core`, and the `cip-179` codec bundled in, with the full TypeScript
|
|
46
|
+
contract rolled up alongside. One side-effect import registers the element;
|
|
47
|
+
there is no peer dependency and no framework requirement (React, Vue, Svelte,
|
|
48
|
+
Solid, or plain HTML all consume the same element). The heavy sealed-encryption
|
|
49
|
+
code splits into lazy chunks fetched only when someone answers a sealed survey.
|
|
50
|
+
|
|
51
|
+
No build step? Load it from an ES-module CDN (the multi-chunk `dist/` resolves
|
|
52
|
+
fine there):
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<script type="module">
|
|
56
|
+
import "https://esm.sh/cardano-tessera-respond@0.1";
|
|
57
|
+
</script>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Inside this repository the workspace exposes two more entries: the root export
|
|
61
|
+
resolves to TypeScript **source** (the composable pieces — `RespondRoot`,
|
|
62
|
+
`adoptWidgetStyles`), and `/element` is the registering Solid TSX entry. They
|
|
63
|
+
are workspace-internal: the app consumes the source so `vite-plugin-solid`
|
|
64
|
+
compiles it against a single Solid instance
|
|
65
|
+
(see [Single Solid instance](#single-solid-instance)); npm installers get only
|
|
66
|
+
the artifact. Don't try to import the raw `/element` TSX from a non-Solid
|
|
67
|
+
bundler — without the Solid transform it either fails to build or renders once
|
|
68
|
+
and never reacts.
|
|
69
|
+
|
|
70
|
+
**Build format is ES only, by design.** `vite build` runs Rollup, which cannot
|
|
71
|
+
code-split UMD/IIFE output — a UMD target would force the lazy tlock/evolution
|
|
72
|
+
chunks to inline and defeat the whole point. Script-tag hosts therefore use
|
|
73
|
+
`<script type="module">`; there is no UMD build.
|
|
74
|
+
|
|
75
|
+
**SSR hosts (SvelteKit, Next.js, …).** Importing the artifact without a DOM is
|
|
76
|
+
a no-op — module evaluation touches no `window`, and registration is guarded —
|
|
77
|
+
so a plain top-level `import "cardano-tessera-respond"` is safe in
|
|
78
|
+
server-rendered modules; no client-only dynamic import is needed. The server
|
|
79
|
+
serializes `<tessera-respond>` as an unknown (empty) element; it upgrades once
|
|
80
|
+
the bundle runs client-side and renders when your code assigns the required
|
|
81
|
+
props after hydration.
|
|
82
|
+
|
|
83
|
+
## Quick start (plain HTML, no bundler)
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<script type="module">
|
|
87
|
+
// Registers <tessera-respond>. Import the package (bundler), a CDN URL,
|
|
88
|
+
// or a relative path to its dist/ as here.
|
|
89
|
+
import "./node_modules/cardano-tessera-respond/dist/tessera-respond.es.js";
|
|
90
|
+
|
|
91
|
+
const el = document.querySelector("tessera-respond");
|
|
92
|
+
|
|
93
|
+
// Object-valued props are set as DOM PROPERTIES, not attributes.
|
|
94
|
+
el.definition = surveyDefinition; // the SurveyDefinition you fetched
|
|
95
|
+
el.surveyRef = { txId, index }; // where it lives on-chain
|
|
96
|
+
// Each role the user can answer as → its credential. Keys are the cip-179
|
|
97
|
+
// Role numbers: Keyholder 4, Stakeholder 3, DRep 0, SPO 1, CC 2.
|
|
98
|
+
el.responder = { 4: paymentCred, 3: stakeCred, 0: drepCred };
|
|
99
|
+
el.tipEpoch = currentEpoch; // from your chain-tip source
|
|
100
|
+
|
|
101
|
+
// The widget emits; you attach + sign + submit.
|
|
102
|
+
el.addEventListener("tessera:response", async (e) => {
|
|
103
|
+
const { payload, proveCredentials } = e.detail;
|
|
104
|
+
// 1. attach `payload` at metadata label 17
|
|
105
|
+
// 2. prove each proveCredentials[i].credential in the tx — via
|
|
106
|
+
// required_signers (mechanism A) or a governance vote (mechanism B)
|
|
107
|
+
// 3. sign with the wallet, submit
|
|
108
|
+
});
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<tessera-respond></tessera-respond>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Until every **required** prop (`definition`, `surveyRef`, `responder`,
|
|
115
|
+
`tipEpoch`) is present, the element renders nothing — so writing
|
|
116
|
+
`<tessera-respond>` in static HTML and assigning props later (as above) is the
|
|
117
|
+
intended pattern; it upgrades and connects before your script runs.
|
|
118
|
+
|
|
119
|
+
For a full **bundler + wallet + real submission** example, see the reference host
|
|
120
|
+
in the app: [`frontend/app/src/ui/screens/DevWidgetHost.tsx`](../../frontend/app/src/ui/screens/DevWidgetHost.tsx).
|
|
121
|
+
It embeds `<tessera-respond>` exactly as a third-party integrator would and closes
|
|
122
|
+
the loop the widget leaves open — proving `proveCredentials` via `required_signers`
|
|
123
|
+
(mechanism A) and signing + submitting through a CIP-30 wallet.
|
|
124
|
+
|
|
125
|
+
## Props
|
|
126
|
+
|
|
127
|
+
Object-valued props (everything but `locale`, `layout`, `cancelled`, `tipEpoch`,
|
|
128
|
+
`initialRole`) **must be set as DOM properties** (`el.definition = …`), never as
|
|
129
|
+
HTML attributes. `solid-element` exposes each as a reactive prop with a
|
|
130
|
+
hyphenated attribute alias, so the string/number/boolean ones (`locale`,
|
|
131
|
+
`layout`, `tip-epoch`, `initial-role`, `cancelled`) _may_ also be written as
|
|
132
|
+
plain attributes. `cancelled` also works HTML boolean-attribute style — a bare
|
|
133
|
+
`<tessera-respond cancelled>` means cancelled.
|
|
134
|
+
|
|
135
|
+
| Prop | Type | Req. | Default | Notes |
|
|
136
|
+
| :---------------- | :------------------------------ | :--: | :----------------- | :----------------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| `definition` | `SurveyDefinition` | ✓ | — | On-chain, or host-enriched with off-chain labels. The widget never fetches — you supply the display definition. |
|
|
138
|
+
| `surveyRef` | `SurveyRef` (`{ txId, index }`) | ✓ | — | Where the definition lives on-chain. Rides back out on every result and into the built response. |
|
|
139
|
+
| `responder` | `Responder` | ✓ | — | Who's answering — see [The responder](#the-responder--eligibility). |
|
|
140
|
+
| `tipEpoch` | `number` | ✓ | — | Current chain-tip epoch. The widget derives open/ended and blocks a closed survey. A snapshot — re-set to refresh. |
|
|
141
|
+
| `cancelled` | `boolean` | | `false` | A valid on-chain cancellation exists (you checked); renders the cancelled state, no form. |
|
|
142
|
+
| `priorResponses` | `readonly SurveyResponse[]` | | — | Prefill for an edit/replace flow — see [Prior responses](#prior-responses-editreplace). |
|
|
143
|
+
| `rationaleAnchor` | `ContentAnchor` | | — | A host-pre-pinned rationale (CIP-179 key 5). The widget never pins to IPFS. |
|
|
144
|
+
| `locale` | `string` (BCP-47) | | `"en"` | Drives both strings and number/date formatting — see [Internationalization](#internationalization). |
|
|
145
|
+
| `messages` | `DeepPartial<RespondMessages>` | | — | Deep-merged string overrides, or a whole unshipped language. |
|
|
146
|
+
| `theme` | `Record<string, string>` | | — | Design-token overrides, reflected as `--tessera-<key>` on the host — see [Theming](#theming--fonts). |
|
|
147
|
+
| `layout` | `"one-per-screen" \| "list"` | | `"one-per-screen"` | Stepper (one question at a time) or all questions at once. |
|
|
148
|
+
| `initialRole` | `Role` | | — | Initial role when the responder is eligible in several. The user can still switch. |
|
|
149
|
+
|
|
150
|
+
## Events
|
|
151
|
+
|
|
152
|
+
All three are dispatched on the element with `bubbles: true, composed: true`, so a
|
|
153
|
+
listener anywhere up the light-DOM tree catches them across the shadow boundary.
|
|
154
|
+
|
|
155
|
+
| Event | `detail` | When |
|
|
156
|
+
| :----------------- | :--------------------- | :------------------------------------------------------------------------------------------ |
|
|
157
|
+
| `tessera:response` | `RespondResult` | The user finalizes a valid answer set. **The widget does not submit** — you do. |
|
|
158
|
+
| `tessera:change` | `RespondChangeDetail` | Any answer edit — `{ decided, total, valid }`, for a host-driven submit button. |
|
|
159
|
+
| `tessera:invalid` | `RespondInvalidDetail` | Validation failed on submit — `{ problems, messages }` (structured codes + localized text). |
|
|
160
|
+
|
|
161
|
+
### `RespondResult` — the payload to submit
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
interface RespondResult {
|
|
165
|
+
surveyRef: SurveyRef;
|
|
166
|
+
role: Role;
|
|
167
|
+
credential: Credential;
|
|
168
|
+
payload: Metadatum; // attach at metadata label 17
|
|
169
|
+
proveCredentials: CredentialProof[]; // prove each in the carrying tx
|
|
170
|
+
sealed: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface CredentialProof {
|
|
174
|
+
credential: Credential;
|
|
175
|
+
keyKind: "payment" | "stake" | "drep" | "pool" | "cc"; // which key proves it
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`payload` is a generic `Metadatum` tree (**not** CBOR bytes) — hand it to your
|
|
180
|
+
Cardano library's metadatum serializer at label 17. Each `proveCredentials` entry
|
|
181
|
+
names a credential the transaction must **prove** control of, and `keyKind` tells
|
|
182
|
+
you which key does so (e.g. the stake key when answering as Stakeholder).
|
|
183
|
+
|
|
184
|
+
### Proving credentials
|
|
185
|
+
|
|
186
|
+
CIP-179 authenticates a response's credential through its carrying transaction by
|
|
187
|
+
**either** of two mechanisms — the widget only _declares_ the credentials; **how**
|
|
188
|
+
you prove them is yours:
|
|
189
|
+
|
|
190
|
+
- **Mechanism A** — put the credential in the transaction's `required_signers`
|
|
191
|
+
(or satisfy its native script in the witness set). Works for every role. This is
|
|
192
|
+
the simple, always-available path, and what the reference host does via
|
|
193
|
+
`frontend/app/src/wallet/submit.ts` (`buildTx` adds each credential with
|
|
194
|
+
`tx.addSigner`).
|
|
195
|
+
- **Mechanism B** (governance-linked surveys only) — the **same transaction**
|
|
196
|
+
casts a qualifying **governance vote** with that very credential on one of the
|
|
197
|
+
survey's linked actions, the Conway voter tag matching the claimed role (CC,
|
|
198
|
+
DRep, SPO). No `required_signers` entry is then needed for it — the vote binding
|
|
199
|
+
is the proof. Only DRep / SPO / CC can bind this way; Stakeholder and Keyholder
|
|
200
|
+
have no voter tag and must use mechanism A.
|
|
201
|
+
|
|
202
|
+
So a DRep answering a governance-linked survey in the very transaction that votes
|
|
203
|
+
on its linked action is already proven by that vote (mechanism B); in any other
|
|
204
|
+
case, add the credential to `required_signers` (mechanism A). `keyKind` is the same
|
|
205
|
+
hint either way — it tells you which key signs, whether that signature lands in
|
|
206
|
+
`required_signers` or in the vote's witness.
|
|
207
|
+
|
|
208
|
+
## TypeScript
|
|
209
|
+
|
|
210
|
+
Importing anything from the package (even a type) loads global augmentations:
|
|
211
|
+
`HTMLElementTagNameMap` gains `"tessera-respond"`, so
|
|
212
|
+
`document.createElement("tessera-respond")` and tag-name queries return a
|
|
213
|
+
`TesseraRespondElement` — every prop a writable, possibly-`undefined` DOM
|
|
214
|
+
property — and `HTMLElementEventMap` gains the three `tessera:*` events, so
|
|
215
|
+
`addEventListener("tessera:response", (e) => …)` types `e.detail` as
|
|
216
|
+
`RespondResult` on any element (they bubble composed, so ancestors count).
|
|
217
|
+
No hand-rolled element interface or `IntrinsicElements` entry is needed.
|
|
218
|
+
|
|
219
|
+
The one exception is Solid's JSX: the published types deliberately augment
|
|
220
|
+
nothing framework-specific (a `declare module "solid-js"` block would break
|
|
221
|
+
type-checking for the many hosts that don't have `solid-js` installed). A
|
|
222
|
+
Solid host wanting `<tessera-respond>` in JSX adds the entry itself:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
declare module "solid-js" {
|
|
226
|
+
namespace JSX {
|
|
227
|
+
interface IntrinsicElements {
|
|
228
|
+
"tessera-respond": JSX.HTMLAttributes<TesseraRespondElement>;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
(In-workspace, `/element` ships that augmentation from source.)
|
|
235
|
+
|
|
236
|
+
## The responder & eligibility
|
|
237
|
+
|
|
238
|
+
`responder` maps each role the user can answer as to the credential that role
|
|
239
|
+
carries — one uniform map (no wallet shape, no provenance flags):
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
type Responder = Partial<Record<Role, Credential>>;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
- **Keyholder / Stakeholder / DRep** come from a connected wallet — its
|
|
246
|
+
payment / stake / DRep credentials. Deriving them is yours; the widget never
|
|
247
|
+
touches a wallet. The Tessera app does it in
|
|
248
|
+
[`frontend/app/src/domain/roles.ts`](../../frontend/app/src/domain/roles.ts)
|
|
249
|
+
(`walletResponder`), which you can copy.
|
|
250
|
+
- **SPO and CC** need cold keys a browser wallet can't hold, so they're never
|
|
251
|
+
wallet-derivable — add them as extra entries when your host vouches for one. A
|
|
252
|
+
responder can be entirely host-supplied (an SPO credential and nothing else).
|
|
253
|
+
- The widget takes the map **verbatim** and never validates it — it lists each
|
|
254
|
+
chosen credential in `proveCredentials` for you to prove through the carrying
|
|
255
|
+
transaction (see [Proving credentials](#proving-credentials)).
|
|
256
|
+
- **The widget picks the role internally.** A responder eligible in several roles
|
|
257
|
+
gets a role picker; the emitted `role` + `credential` reflect the one chosen.
|
|
258
|
+
The role → signing-key mapping is fixed: Keyholder→`payment`,
|
|
259
|
+
Stakeholder→`stake`, DRep→`drep`, SPO→`pool`, CC→`cc`.
|
|
260
|
+
|
|
261
|
+
In-workspace, `cardano-tessera-respond-core` exports
|
|
262
|
+
`respondableRolesFor(def, responder)` and `credentialForRole(role, responder)`
|
|
263
|
+
to compute eligibility host-side too (e.g. to decide whether to mount the
|
|
264
|
+
widget at all). That package is not published yet — the planned host toolkit
|
|
265
|
+
(`cardano-tessera-host`) will carry these helpers for npm integrators.
|
|
266
|
+
|
|
267
|
+
## Prior responses (edit/replace)
|
|
268
|
+
|
|
269
|
+
For an edit/replace flow, pass the responder's prior responses in
|
|
270
|
+
`priorResponses` — **one per role** they've answered as.
|
|
271
|
+
|
|
272
|
+
The role is chosen _inside_ the widget, so the host can't know up front which
|
|
273
|
+
prior response applies: pass them all, and the widget selects the one matching the
|
|
274
|
+
current role + credential, re-prefilling as the user switches roles. A responder
|
|
275
|
+
who answered as both DRep and Stakeholder passes both; the array is
|
|
276
|
+
order-independent.
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
// One deduped "mine" set, resolved to a prior response per respondable role.
|
|
280
|
+
el.priorResponses = respondableRolesFor(def, responder).flatMap((r) => {
|
|
281
|
+
const cred = credentialForRole(r, responder);
|
|
282
|
+
const prior = cred ? findPriorResponse(mine, surveyRef, r, cred) : undefined;
|
|
283
|
+
return prior ? [prior] : [];
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Pass **sealed** priors too. Their ciphertext can't prefill the form, but the
|
|
288
|
+
widget still marks it as a replacement — on a sealed survey that's the only way
|
|
289
|
+
a responder learns they already answered. The reference host `DevWidgetHost.tsx`
|
|
290
|
+
does exactly this against a survey bundle.
|
|
291
|
+
|
|
292
|
+
## What the host must do
|
|
293
|
+
|
|
294
|
+
The widget deliberately leaves these to you:
|
|
295
|
+
|
|
296
|
+
1. **Attach + submit.** On `tessera:response`, put `payload` at metadata label 17,
|
|
297
|
+
prove each `proveCredentials[].credential` in the transaction — via
|
|
298
|
+
`required_signers` or a governance-vote binding (see
|
|
299
|
+
[Proving credentials](#proving-credentials)) — sign with the indicated key
|
|
300
|
+
kinds, and submit.
|
|
301
|
+
2. **Network conformance.** The responder carries no network id, so the widget
|
|
302
|
+
can't tell whether the wallet is on the wrong chain. Check
|
|
303
|
+
`walletNetworkId === targetNetwork` before you submit.
|
|
304
|
+
3. **Tip freshness.** `tipEpoch` is a snapshot; a survey can close between your
|
|
305
|
+
last refresh and the user's submit. Re-set the prop to refresh. The chain (and
|
|
306
|
+
the tally's eligibility rules) is the final arbiter regardless — the emitted
|
|
307
|
+
payload is still only a payload.
|
|
308
|
+
4. **Only mount for an open survey.** The widget blocks a closed/cancelled survey
|
|
309
|
+
from its own gate, but you decide whether to show it at all — pass an accurate
|
|
310
|
+
`tipEpoch` and `cancelled`.
|
|
311
|
+
5. **Enrich the definition.** Any off-chain label enrichment (IPFS presentation
|
|
312
|
+
docs, etc.) happens upstream; the widget renders the `definition` you give it.
|
|
313
|
+
6. **Pin a rationale** (if used). Pass a pre-pinned `ContentAnchor` in
|
|
314
|
+
`rationaleAnchor`; the widget attaches it but never pins.
|
|
315
|
+
|
|
316
|
+
## Sealed surveys
|
|
317
|
+
|
|
318
|
+
For a survey with `submissionMode.type === "sealed"`, the widget owns encryption
|
|
319
|
+
end-to-end: it timelock-encrypts the answers to the survey's drand round
|
|
320
|
+
(`cip-179/tlock`) before emitting, so the sealed `payload` you receive is already
|
|
321
|
+
the final ciphertext wrapped in the label-17 envelope — you can't get it wrong.
|
|
322
|
+
`sealed: true` on the result marks it.
|
|
323
|
+
|
|
324
|
+
- The heavy sealing code (tlock + the evolution CBOR codec) is **lazy-loaded** only
|
|
325
|
+
when a sealed survey is answered. The public path never loads it. In a built
|
|
326
|
+
bundle these are separate chunks fetched on demand.
|
|
327
|
+
- **Quicknet only.** cip-179 supports only the drand quicknet chain today, and
|
|
328
|
+
`sealAnswers` encrypts to quicknet unconditionally. A sealed definition pinned to
|
|
329
|
+
any other drand chain would produce a permanently undecryptable response, so the
|
|
330
|
+
widget **blocks submission** (rather than mis-encrypting) and shows a notice.
|
|
331
|
+
|
|
332
|
+
## Internationalization
|
|
333
|
+
|
|
334
|
+
Hybrid strategy driven by two props:
|
|
335
|
+
|
|
336
|
+
- **`locale`** (BCP-47, default `"en"`) selects a bundled catalog. **`en` and `fr`
|
|
337
|
+
ship.** It also drives all number and date formatting via `Intl`, keyed on the
|
|
338
|
+
locale — including the sealed-reveal date.
|
|
339
|
+
- **`messages`** deep-merges string overrides over the bundled catalog, or supplies
|
|
340
|
+
an entirely unshipped language.
|
|
341
|
+
|
|
342
|
+
Numbers always follow `locale`, even when strings fall back to English. If `locale`
|
|
343
|
+
resolves to **no bundled catalog and no `messages`**, the widget emits a one-time
|
|
344
|
+
`console.warn` in dev builds (e.g. _"locale 'de' has no bundled catalog and no
|
|
345
|
+
`messages`; UI text will render in English."_) — the number/date formatting is
|
|
346
|
+
still locale-correct, only the prose falls back.
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
el.locale = "fr"; // bundled
|
|
350
|
+
el.locale = "de";
|
|
351
|
+
el.messages = { respond: { signAndSubmit: "Absenden" } /* … */ }; // supply/override strings
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Theming & fonts
|
|
355
|
+
|
|
356
|
+
Every color, radius, and font is a `--tessera-*` CSS custom property defined on
|
|
357
|
+
`:host` inside the shadow root (defaults ported from the app's design tokens).
|
|
358
|
+
Custom properties inherit _through_ the shadow boundary, so you override them two
|
|
359
|
+
equivalent ways:
|
|
360
|
+
|
|
361
|
+
```css
|
|
362
|
+
/* Host CSS — target the element. */
|
|
363
|
+
tessera-respond {
|
|
364
|
+
--tessera-accent: #6b46ff;
|
|
365
|
+
--tessera-role-drep: #6b46ff;
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
// The `theme` prop — each entry reflected as `--tessera-<key>` inline on :host.
|
|
371
|
+
el.theme = { accent: "#6b46ff", "role-drep": "#6b46ff" };
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Role chips have their own tokens (`--tessera-role-drep`, `--tessera-role-spo`,
|
|
375
|
+
`--tessera-role-cc`, `--tessera-role-stakeholder`, `--tessera-role-keyholder`, each
|
|
376
|
+
with a `-bg` pair). See [`src/theme.css`](./src/theme.css) for the full token list.
|
|
377
|
+
|
|
378
|
+
- **Dark mode.** You can re-skin every color through the tokens, but native form
|
|
379
|
+
controls (range track, number spinners) also need `color-scheme: dark`, which a
|
|
380
|
+
custom property cannot express. Set it in host CSS on the element (or a wrapper):
|
|
381
|
+
`tessera-respond { color-scheme: dark; }`. The `:host` root is transparent, so it
|
|
382
|
+
takes your page's background.
|
|
383
|
+
- **Fonts.** The widget **names** fonts via `--tessera-sans` / `--tessera-mono` /
|
|
384
|
+
`--tessera-serif` but **never loads them** — loading webfonts is the host page's
|
|
385
|
+
job (a shadow root can't reach your `@font-face` rules for _loading_, only for
|
|
386
|
+
_using_ already-loaded families). Point these tokens at families your page has
|
|
387
|
+
loaded, or leave the system-font fallbacks.
|
|
388
|
+
|
|
389
|
+
## Layout
|
|
390
|
+
|
|
391
|
+
- **`"one-per-screen"`** (default) — a stepper: one question at a time with
|
|
392
|
+
prev/next and progress dots.
|
|
393
|
+
- **`"list"`** — every question rendered at once.
|
|
394
|
+
|
|
395
|
+
Both reuse the same body components, the same `decided()` gating, and the same
|
|
396
|
+
answer collection; layout is pure presentation over shared state.
|
|
397
|
+
|
|
398
|
+
## Single Solid instance
|
|
399
|
+
|
|
400
|
+
The widget is built on SolidJS. A bundler host that also uses Solid (or embeds
|
|
401
|
+
another Solid custom element) must ensure a **single** Solid runtime instance —
|
|
402
|
+
two copies break reactivity. Under pnpm this usually happens automatically
|
|
403
|
+
(shared symlink); with a bundler, add `solid-js` and `solid-element` to your
|
|
404
|
+
`resolve.dedupe`. The Tessera app does exactly this in
|
|
405
|
+
[`frontend/app/vite.config.ts`](../../frontend/app/vite.config.ts). A script-tag
|
|
406
|
+
host loading the self-contained artifact has no such concern — Solid is bundled in.
|
|
407
|
+
|
|
408
|
+
## One copy per page
|
|
409
|
+
|
|
410
|
+
Load the widget from a **single** source, once. `<tessera-respond>` is a global
|
|
411
|
+
custom-element name, and a second copy of the package landing on the same page
|
|
412
|
+
(the script-tag artifact _plus_ a bundler copy, or two different versions) does
|
|
413
|
+
**not** error: the second registration swaps its component into the class the
|
|
414
|
+
first copy defined, so new connections render the second copy's UI against the
|
|
415
|
+
**first** copy's prop/attribute wiring. A prop that only the newer version
|
|
416
|
+
declares is then silently non-reactive. If you mix consumption styles or
|
|
417
|
+
versions, dedupe them to one before anything registers the element.
|
|
418
|
+
|
|
419
|
+
## Development
|
|
420
|
+
|
|
421
|
+
```sh
|
|
422
|
+
pnpm install
|
|
423
|
+
pnpm --filter cardano-tessera-respond dev # Vite dev harness (dev/index.html)
|
|
424
|
+
pnpm --filter cardano-tessera-respond build # emits dist/ (bundle + chunks + rolled-up d.ts)
|
|
425
|
+
pnpm --filter cardano-tessera-respond test # builds, then runs the artifact + token tests
|
|
426
|
+
pnpm --filter cardano-tessera-respond type-check
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Two demo/test surfaces cover different failure modes:
|
|
430
|
+
|
|
431
|
+
| Path | What it drives |
|
|
432
|
+
| :---------------------- | :-------------------------------------------------------------------------------------------------------------------- |
|
|
433
|
+
| `dev/` | Vite dev harness — `RespondRoot` in a shadow root, real workspace source, all samples/locales/themes. |
|
|
434
|
+
| `demo/index.html` | Plain-HTML host for the **built** bundle, fully offline. Shows the sealed chunks lazy-loading in the network tab. |
|
|
435
|
+
| `test/artifact.test.ts` | The automated twin of `demo/` — mounts the built artifact in happy-dom, offline, asserts the whole prop → event loop. |
|
|
436
|
+
| `test/tokens.test.ts` | Gate: every `--tessera-*` referenced in the CSS is defined in `theme.css`, and vice-versa. |
|
|
437
|
+
|
|
438
|
+
In the workspace the package is consumed straight from `src` (its `exports` map
|
|
439
|
+
points at TypeScript source, like every sibling), so cross-package edits are live
|
|
440
|
+
with no build step.
|
|
441
|
+
|
|
442
|
+
## Layout
|
|
443
|
+
|
|
444
|
+
| Path | Purpose |
|
|
445
|
+
| :---------------- | :------------------------------------------------------------------------ |
|
|
446
|
+
| `src/element.tsx` | `customElement("tessera-respond", …)` registration + prop reflection. |
|
|
447
|
+
| `src/Respond.tsx` | The trimmed answering component (props in → `tessera:*` events out). |
|
|
448
|
+
| `src/roles.ts` | Role label/color/keyKind presentation over the injected catalog + tokens. |
|
|
449
|
+
| `src/theme.css` | The `--tessera-*` design-token defaults on `:host`. |
|
|
450
|
+
| `src/styles/` | Component CSS, shadow-scoped plain CSS delivered via `?inline`. |
|
|
451
|
+
| `src/types.ts` | The public prop/event TypeScript contract. |
|
|
452
|
+
|
|
453
|
+
The per-question body components live in
|
|
454
|
+
[`cardano-tessera-respond-ui`](../respond-ui/README.md), shared with the app; see
|
|
455
|
+
[`cardano-tessera-respond-core`](../respond-core/README.md) for the pure logic both
|
|
456
|
+
share.
|