@provex/eas 1.9.2-rc.20260721141727.45132a5 → 1.9.2

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,212 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { encodePacked, keccak256, zeroAddress } from 'viem';
4
+ import { privateKeyToAccount } from 'viem/accounts';
5
+ import { buildOffchainAgeAttestation, buildOffchainAgeAttestationForChain, encodeAgeClaim, } from './eas-builder.js';
6
+ import { decodeAgeClaim } from './offchain-verify.js';
7
+ import { EAS_ADDRESSES_BY_CHAIN_ID, EAS_MAINNET_ADDRESS, PROVEX_AGE_SCHEMA, PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID, getAgeSchemaUid, getAgeSchemaUidOrPlaceholder, } from './eas.js';
8
+ const ZERO_BYTES32 = ('0x' + '0'.repeat(64));
9
+ /** Deterministic unique identifier of the age schema — kept in lockstep with eas.ts. */
10
+ const AGE_UID = '0x4ffe2fd75afc43a65b604589606c0996db197fcae4f834d240e3c023c40f297d';
11
+ /** Deterministic unique identifiers of the OTHER two schemas, used as miswiring canaries. */
12
+ const IDENTITY_UID_369 = '0xdda0f5748fee977851ba75a82b2d378dcdc20b2a816278ff55e411852c4a4092';
13
+ const RELEASE_UID_369 = '0xc9bf5f19b85d6a84403428964837965e16f72d40863f5d9351d3951249f0d043';
14
+ const CLAIM = {
15
+ nullifier: ('0x' + 'ab'.repeat(32)),
16
+ overN: true,
17
+ threshold: 21,
18
+ identityVerified: true,
19
+ expiresAt: 2000000000n,
20
+ };
21
+ describe('PROVEX_AGE_SCHEMA', () => {
22
+ it('encodes no personally identifying field', () => {
23
+ // The privacy invariant, as an executable check: a future edit that adds a
24
+ // name / date of birth / Social Security field to the schema fails HERE,
25
+ // loudly, rather than silently shipping personal data inside every
26
+ // attestation this schema produces.
27
+ for (const banned of ['name', 'handle', 'birth', 'dob', 'ssn', 'address ']) {
28
+ assert.ok(!PROVEX_AGE_SCHEMA.toLowerCase().includes(banned), `PROVEX_AGE_SCHEMA must not carry a "${banned}" field`);
29
+ }
30
+ });
31
+ it('is comma-separated typed fields in the documented order', () => {
32
+ // Field order is load-bearing: it fixes the encoding of every attestation
33
+ // issued under this schema, and the decoder reads positionally.
34
+ const fields = PROVEX_AGE_SCHEMA.split(',').map((field) => field.trim());
35
+ assert.deepEqual(fields, [
36
+ 'bytes32 nullifier',
37
+ 'bool overN',
38
+ 'uint8 threshold',
39
+ 'bool identityVerified',
40
+ 'uint64 expiresAt',
41
+ ]);
42
+ });
43
+ });
44
+ describe('age claim round-trip', () => {
45
+ it('decodes back to the same claim it encoded', () => {
46
+ assert.deepEqual(decodeAgeClaim(encodeAgeClaim(CLAIM)), CLAIM);
47
+ });
48
+ it('preserves a false overN rather than dropping the field', () => {
49
+ // WHY: "this person is NOT over the threshold" is a real, signed answer —
50
+ // not a missing field. A decoder that coerced it away (or an encoder that
51
+ // omitted it) would turn a refusal into an absence, which a consuming
52
+ // application could misread as "unknown" and then fail open.
53
+ const minor = { ...CLAIM, overN: false };
54
+ assert.equal(decodeAgeClaim(encodeAgeClaim(minor)).overN, false);
55
+ assert.deepEqual(decodeAgeClaim(encodeAgeClaim(minor)), minor);
56
+ });
57
+ it('round-trips both identityVerified states independently of overN', () => {
58
+ // The two booleans are adjacent in the schema; a field-order slip would
59
+ // swap them and silently mislabel an unverified identity as verified.
60
+ const claim = { ...CLAIM, overN: true, identityVerified: false };
61
+ const decoded = decodeAgeClaim(encodeAgeClaim(claim));
62
+ assert.equal(decoded.overN, true);
63
+ assert.equal(decoded.identityVerified, false);
64
+ });
65
+ it('round-trips the uint8 threshold at both boundaries', () => {
66
+ // 0 and 255 are the extremes the uint8 field can hold. 0 must not be
67
+ // confused with "unset", and 255 must not overflow into the next word.
68
+ for (const threshold of [0, 255]) {
69
+ const claim = { ...CLAIM, threshold };
70
+ assert.equal(decodeAgeClaim(encodeAgeClaim(claim)).threshold, threshold);
71
+ }
72
+ });
73
+ it('round-trips an expiresAt at the uint64 ceiling', () => {
74
+ const claim = { ...CLAIM, expiresAt: 2n ** 64n - 1n };
75
+ assert.equal(decodeAgeClaim(encodeAgeClaim(claim)).expiresAt, 2n ** 64n - 1n);
76
+ });
77
+ });
78
+ describe('encodeAgeClaim', () => {
79
+ it('produces deterministic bytes for the same claim', () => {
80
+ assert.equal(encodeAgeClaim(CLAIM), encodeAgeClaim(CLAIM));
81
+ assert.match(encodeAgeClaim(CLAIM), /^0x[0-9a-fA-F]+$/);
82
+ });
83
+ it('produces different bytes when the nullifier differs', () => {
84
+ // WHY: the nullifier is what enforces one credential per person. If two
85
+ // different nullifiers encoded identically, the uniqueness guarantee
86
+ // would collapse.
87
+ assert.notEqual(encodeAgeClaim(CLAIM), encodeAgeClaim({ ...CLAIM, nullifier: ('0x' + 'cd'.repeat(32)) }));
88
+ });
89
+ it('produces different bytes when the threshold differs', () => {
90
+ // WHY: an "over 18" credential must not be byte-identical to an "over 21"
91
+ // one, or a signature over the former would verify as the latter.
92
+ assert.notEqual(encodeAgeClaim(CLAIM), encodeAgeClaim({ ...CLAIM, threshold: 18 }));
93
+ });
94
+ });
95
+ describe('getAgeSchemaUid', () => {
96
+ it('resolves the pinned unique identifier on the Provex chains', () => {
97
+ assert.equal(getAgeSchemaUid(369), AGE_UID);
98
+ assert.equal(getAgeSchemaUid(943), AGE_UID);
99
+ assert.equal(getAgeSchemaUid(11155111), AGE_UID);
100
+ });
101
+ it('throws while the per-chain unique identifier is still the placeholder', () => {
102
+ // Ethereum mainnet (1) stays a placeholder — there is no Provex deployment there.
103
+ assert.throws(() => getAgeSchemaUid(1), /Provex schema not registered on chainId 1/);
104
+ });
105
+ it('throws for chains with no unique-identifier slot at all', () => {
106
+ assert.throws(() => getAgeSchemaUid(999_999), /no schema UID slot for chainId 999999/);
107
+ });
108
+ it('returns the override when EAS_AGE_SCHEMA_UID_<chainId>_OVERRIDE is set', () => {
109
+ const override = '0x' + '99'.repeat(32);
110
+ process.env.EAS_AGE_SCHEMA_UID_369_OVERRIDE = override;
111
+ try {
112
+ assert.equal(getAgeSchemaUid(369), override);
113
+ }
114
+ finally {
115
+ delete process.env.EAS_AGE_SCHEMA_UID_369_OVERRIDE;
116
+ }
117
+ });
118
+ });
119
+ describe('getAgeSchemaUidOrPlaceholder', () => {
120
+ it('returns the all-zero placeholder for unregistered or unknown chains', () => {
121
+ assert.equal(getAgeSchemaUidOrPlaceholder(1), ZERO_BYTES32);
122
+ assert.equal(getAgeSchemaUidOrPlaceholder(999_999), ZERO_BYTES32);
123
+ });
124
+ it('returns the pinned unique identifier for the Provex chains', () => {
125
+ assert.equal(getAgeSchemaUidOrPlaceholder(369), AGE_UID);
126
+ });
127
+ });
128
+ describe('PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID', () => {
129
+ it('pins the deterministic unique identifier the schema string produces', () => {
130
+ // An Ethereum Attestation Service schema unique identifier is
131
+ // keccak256(abi.encodePacked(schema, resolver, revocable)) — computable
132
+ // with no transaction. WHY this assertion: it makes editing the schema
133
+ // string without re-pinning the identifier fail the suite, instead of
134
+ // silently signing under an identifier that no registration would match.
135
+ const derived = keccak256(encodePacked(['string', 'address', 'bool'], [PROVEX_AGE_SCHEMA, zeroAddress, true]));
136
+ assert.equal(derived, AGE_UID);
137
+ assert.equal(PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID[369], derived);
138
+ });
139
+ it('is distinct from the identity and release schema unique identifiers', () => {
140
+ // Three schemas share one registry unique-identifier space; a collision
141
+ // would let an age attestation be read as an identity or release one.
142
+ assert.notEqual(AGE_UID, IDENTITY_UID_369);
143
+ assert.notEqual(AGE_UID, RELEASE_UID_369);
144
+ });
145
+ it('has the same chain keys as the Ethereum Attestation Service address map', () => {
146
+ assert.deepEqual(Object.keys(PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID).sort(), Object.keys(EAS_ADDRESSES_BY_CHAIN_ID).sort());
147
+ });
148
+ });
149
+ describe('buildOffchainAgeAttestation', () => {
150
+ // Hardhat's deterministic account 0 — the same key the identity and release
151
+ // builder tests use, so the address/key pair is recognizable at a glance.
152
+ const ATTESTER_KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d';
153
+ const HOLDER = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8';
154
+ const AGE_SCHEMA_UID = ('0x' + '77'.repeat(32));
155
+ it('binds the credential to the holder wallet as recipient', async () => {
156
+ // WHY: unlike a release attestation (subject is a manifest, recipient is
157
+ // the zero address), an age credential is ABOUT a person. The recipient is
158
+ // the only thing that tells a consuming application whose credential it is.
159
+ const attester = privateKeyToAccount(ATTESTER_KEY);
160
+ const result = await buildOffchainAgeAttestation({
161
+ claim: CLAIM,
162
+ attester,
163
+ recipient: HOLDER,
164
+ eas: EAS_MAINNET_ADDRESS,
165
+ schemaUid: AGE_SCHEMA_UID,
166
+ chainId: 1,
167
+ });
168
+ assert.match(result.signature, /^0x[0-9a-fA-F]{130}$/);
169
+ assert.equal(result.message.recipient.toLowerCase(), HOLDER.toLowerCase());
170
+ assert.notEqual(result.message.recipient.toLowerCase(), zeroAddress);
171
+ assert.equal(result.message.schema, AGE_SCHEMA_UID);
172
+ assert.equal(result.domain.chainId, 1);
173
+ });
174
+ it('embeds the encoded age claim and derives the envelope expiry from it', async () => {
175
+ const attester = privateKeyToAccount(ATTESTER_KEY);
176
+ const result = await buildOffchainAgeAttestation({
177
+ claim: CLAIM,
178
+ attester,
179
+ recipient: HOLDER,
180
+ eas: EAS_MAINNET_ADDRESS,
181
+ schemaUid: AGE_SCHEMA_UID,
182
+ chainId: 1,
183
+ });
184
+ assert.equal(result.message.data, encodeAgeClaim(CLAIM));
185
+ // WHY THIS MATTERS: the envelope expiry is DERIVED from claim.expiresAt,
186
+ // never passed alongside it. If a future edit reintroduces a separate
187
+ // expirationTime parameter, these two can drift — a consumer using standard
188
+ // Ethereum Attestation Service tooling would read the envelope while one
189
+ // decoding the claim reads the data, and they would disagree about whether
190
+ // the same credential is still valid, with nothing to flag the split.
191
+ assert.equal(result.message.expirationTime, CLAIM.expiresAt);
192
+ assert.equal(result.message.revocable, true);
193
+ // The signed bytes decode back to the claim the caller handed in — the
194
+ // end-to-end guarantee a verifier depends on.
195
+ assert.deepEqual(decodeAgeClaim(result.message.data), CLAIM);
196
+ });
197
+ it('signs with the age schema unique identifier via the ForChain wrapper', async () => {
198
+ // Proves the wrapper routes through getAgeSchemaUid rather than the
199
+ // identity or release lookup — a miswiring TypeScript cannot catch.
200
+ const attester = privateKeyToAccount(ATTESTER_KEY);
201
+ const result = await buildOffchainAgeAttestationForChain({
202
+ claim: CLAIM,
203
+ attester,
204
+ recipient: HOLDER,
205
+ chainId: 369,
206
+ });
207
+ assert.equal(result.message.schema, AGE_UID);
208
+ assert.notEqual(result.message.schema, IDENTITY_UID_369);
209
+ assert.notEqual(result.message.schema, RELEASE_UID_369);
210
+ assert.equal(result.message.recipient.toLowerCase(), HOLDER.toLowerCase());
211
+ });
212
+ });
@@ -0,0 +1,352 @@
1
+ import * as easSdkNamespace from '@ethereum-attestation-service/eas-sdk';
2
+ import type { Address, Hex, LocalAccount } from 'viem';
3
+ type OffchainAttestationVersionEnum = (typeof easSdkNamespace)['OffchainAttestationVersion'][keyof (typeof easSdkNamespace)['OffchainAttestationVersion']];
4
+ /**
5
+ * @provex/eas — off-chain Ethereum Attestation Service attestation builder.
6
+ *
7
+ * Wraps the upstream `@ethereum-attestation-service/eas-sdk` so callers
8
+ * (notary-gate's webhook) can hand in a viem `LocalAccount` + an
9
+ * `IdentityClaim` and get back a fully-signed off-chain attestation
10
+ * without touching ethers directly.
11
+ *
12
+ * The SDK's signer interface is ethers-shaped (`signTypedData(domain,
13
+ * types, value)`), but we live in viem-land. The thin adapter below
14
+ * translates the SDK's three-arg call into viem's
15
+ * `account.signTypedData({ domain, types, primaryType, message })` and
16
+ * returns the resulting hex signature unchanged. Same EIP-712 wire bytes,
17
+ * different SDK conventions.
18
+ *
19
+ * Output normalization: the SDK returns `signature: { r, s, v }` and
20
+ * `domain.chainId: bigint`. We flatten the signature to a single 65-byte
21
+ * 0x-hex and convert chainId to `number` so downstream JSON serialization
22
+ * (notary-gate's attestationStore → polling UI) is straightforward and
23
+ * consumers can compare chainId without bigint juggling.
24
+ */
25
+ /**
26
+ * Identity claim destined for an off-chain EAS attestation. The `handle`
27
+ * is the proven platform username (e.g. Discord username, X screen_name);
28
+ * the contract-level wire encoding is the EAS schema string in `eas.ts`.
29
+ */
30
+ export type IdentityClaim = {
31
+ platformHash: Hex;
32
+ handle: string;
33
+ user: Address;
34
+ /** Unix seconds at which this attestation expires; encoded as uint64. */
35
+ expiresAt: bigint;
36
+ };
37
+ export type BuildOffchainEasAttestationArgs = {
38
+ claim: IdentityClaim;
39
+ attester: LocalAccount;
40
+ /** EAS contract address on the target chain (used in the EIP-712 domain). */
41
+ eas: Address;
42
+ /** Pinned EAS schema UID — must match `PROVEX_IDENTITY_SCHEMA`. */
43
+ schemaUid: Hex;
44
+ /** EVM chain id the attestation is bound to (1 = Ethereum mainnet). */
45
+ chainId: number;
46
+ };
47
+ /**
48
+ * Final wire shape returned by the builder. Matches `SignedOffchainAttestation`
49
+ * from the EAS SDK with two normalizations applied:
50
+ * - signature flattened to single hex string
51
+ * - domain.chainId widened to plain number for JSON safety
52
+ */
53
+ export type OffchainEasAttestation = {
54
+ version: OffchainAttestationVersionEnum;
55
+ uid: string;
56
+ domain: {
57
+ name: string;
58
+ version: string;
59
+ chainId: number;
60
+ verifyingContract: string;
61
+ };
62
+ primaryType: string;
63
+ types: Record<string, Array<{
64
+ name: string;
65
+ type: string;
66
+ }>>;
67
+ message: {
68
+ version: number;
69
+ schema: string;
70
+ recipient: string;
71
+ time: bigint;
72
+ expirationTime: bigint;
73
+ revocable: boolean;
74
+ refUID: string;
75
+ data: string;
76
+ salt?: string;
77
+ };
78
+ signature: Hex;
79
+ };
80
+ /**
81
+ * EAS-encoded `bytes data` for a Provex identity claim.
82
+ *
83
+ * Pure (no I/O), idempotent, deterministic — the same claim always
84
+ * produces the same hex. Three callers depend on this byte-exact match:
85
+ *
86
+ * - {@link buildOffchainEasAttestation} — bytes signed by the gate's
87
+ * off-chain EIP-712 envelope.
88
+ * - {@link buildDelegatedEasAttestationRequest} — bytes signed by the
89
+ * gate's delegated EIP-712 envelope (sponsored on-chain publish).
90
+ * - `packages/ui/src/lib/identityChain.ts` — bytes the user's own
91
+ * wallet writes via plain `attest()` (self-pay on-chain publish).
92
+ *
93
+ * Centralized so a schema field reorder lands in one place — if any
94
+ * caller drifted out of sync, the on-chain delegated signature would
95
+ * verify against different bytes than the contract was handed, and EAS
96
+ * would revert with `InvalidSignature()`.
97
+ *
98
+ * Field order is load-bearing — it must match `PROVEX_IDENTITY_SCHEMA`
99
+ * in `eas.ts` exactly.
100
+ */
101
+ export declare const encodeIdentityClaim: (claim: IdentityClaim) => Hex;
102
+ /**
103
+ * Default attestation validity window — 90 days, matching the off-chain
104
+ * signing validity in `signOffchainAttestation`. EAS reverts with
105
+ * `InvalidExpirationTime()` when `expirationTime <= block.timestamp`, so
106
+ * any value here must comfortably exceed worst-case mining time even on
107
+ * a congested network. A previous 5-minute window kept tripping that
108
+ * revert when a PulseChain tx took longer than 5 minutes to mine.
109
+ *
110
+ * Exposed as a function (rather than a constant) so callers compute the
111
+ * deadline relative to the moment they sign/submit, not module-load
112
+ * time.
113
+ */
114
+ export declare const attestationExpirationTime: (options?: {
115
+ nowSeconds?: number;
116
+ }) => bigint;
117
+ /**
118
+ * Build a fully-signed off-chain EAS attestation for a Provex identity
119
+ * claim. Pure function modulo the signature: no network I/O, no on-chain
120
+ * verification (the SDK's `verifyOnchain` option is left disabled — we
121
+ * don't need an RPC just to sign).
122
+ *
123
+ * The returned object is JSON-serializable directly — bigints are kept
124
+ * as bigints so the caller decides how to encode them on the wire.
125
+ */
126
+ export declare const buildOffchainEasAttestation: (args: BuildOffchainEasAttestationArgs) => Promise<OffchainEasAttestation>;
127
+ /**
128
+ * Convenience wrapper: resolve `eas` + `schemaUid` from the chainId via
129
+ * `getEasAddress` / `getSchemaUid`, then sign. Throws if either lookup
130
+ * fails (chain unknown, address not pinned, or schema not registered).
131
+ *
132
+ * Prefer this over the raw `buildOffchainEasAttestation` for any
133
+ * caller that can hand a `chainId` — it eliminates the chance of
134
+ * pairing the wrong EAS address with the wrong UID.
135
+ */
136
+ export declare const buildOffchainEasAttestationForChain: (args: Omit<BuildOffchainEasAttestationArgs, "eas" | "schemaUid">) => Promise<OffchainEasAttestation>;
137
+ /**
138
+ * Release claim destined for a gate-signed off-chain Ethereum Attestation
139
+ * Service attestation. The subject is a dapp release, not a user: `dappKey`
140
+ * keys the per-application timeline and `manifestHash` is the exact release
141
+ * proven via Transport-Layer-Security. There is no `expiresAt` field — a
142
+ * release attestation's freshness is the EAS-native `message.expirationTime`,
143
+ * set by the caller.
144
+ */
145
+ export type ReleaseClaim = {
146
+ dappKey: Hex;
147
+ manifestHash: Hex;
148
+ domain: string;
149
+ version: string;
150
+ };
151
+ /**
152
+ * EAS-encoded `bytes data` for a Provex release claim. Pure and
153
+ * deterministic. Field order is load-bearing — it must match
154
+ * `PROVEX_RELEASE_SCHEMA` in `eas.ts` exactly.
155
+ */
156
+ export declare const encodeReleaseClaim: (claim: ReleaseClaim) => Hex;
157
+ export type BuildOffchainReleaseAttestationArgs = {
158
+ claim: ReleaseClaim;
159
+ attester: LocalAccount;
160
+ /** EAS contract address on the target chain (used in the EIP-712 domain). */
161
+ eas: Address;
162
+ /** Pinned EAS schema UID — must match `PROVEX_RELEASE_SCHEMA`. */
163
+ schemaUid: Hex;
164
+ /** EVM chain id the attestation is bound to. */
165
+ chainId: number;
166
+ /** EAS-native freshness horizon (Unix seconds). Use `attestationExpirationTime()`. */
167
+ expirationTime: bigint;
168
+ };
169
+ /**
170
+ * Build a fully-signed off-chain release attestation. The recipient is the
171
+ * zero address — the subject is the manifest (in the encoded data), not a
172
+ * wallet. Gate-signed: the attester is the gate's own account, vouching that
173
+ * it verified a Transport-Layer-Security proof of this release.
174
+ */
175
+ export declare const buildOffchainReleaseAttestation: (args: BuildOffchainReleaseAttestationArgs) => Promise<OffchainEasAttestation>;
176
+ /**
177
+ * Convenience wrapper: resolve `eas` + release `schemaUid` from the chainId,
178
+ * then sign. Throws if either lookup fails (chain unknown, address not
179
+ * pinned, or release schema not registered on that chain).
180
+ */
181
+ export declare const buildOffchainReleaseAttestationForChain: (args: Omit<BuildOffchainReleaseAttestationArgs, "eas" | "schemaUid">) => Promise<OffchainEasAttestation>;
182
+ /**
183
+ * Age + identity claim destined for an off-chain Ethereum Attestation Service
184
+ * attestation. `nullifier` is the one-way, domain-salted uniqueness marker;
185
+ * `overN` answers only "at least `threshold` years old"; `identityVerified`
186
+ * records that the capture proved possession of an identity-proofed account.
187
+ *
188
+ * No personal field is carried — by design. The date of birth, name, Social
189
+ * Security Number, and account identifier stay inside the issuer's trust
190
+ * boundary; only these derived facts escape it. The subject is bound by the
191
+ * attestation's `recipient` (the holder's wallet), not by anything here.
192
+ */
193
+ export type AgeClaim = {
194
+ nullifier: Hex;
195
+ overN: boolean;
196
+ threshold: number;
197
+ identityVerified: boolean;
198
+ /** Unix seconds at which this claim expires; encoded as uint64. */
199
+ expiresAt: bigint;
200
+ };
201
+ /**
202
+ * EAS-encoded `bytes data` for a Provex age claim. Pure and deterministic —
203
+ * no I/O, no side effects, the same claim always produces the same hex.
204
+ *
205
+ * Field order is load-bearing — it must match `PROVEX_AGE_SCHEMA` in `eas.ts`
206
+ * exactly, because `decodeAgeClaim` reads positionally. A reorder here without
207
+ * a matching one there would decode `overN` out of the `identityVerified` slot.
208
+ */
209
+ export declare const encodeAgeClaim: (claim: AgeClaim) => Hex;
210
+ export type BuildOffchainAgeAttestationArgs = {
211
+ claim: AgeClaim;
212
+ attester: LocalAccount;
213
+ /** The holder's wallet — the subject this credential is bound to. */
214
+ recipient: Address;
215
+ /** EAS contract address on the target chain (used in the EIP-712 domain). */
216
+ eas: Address;
217
+ /** Pinned EAS schema UID — must match `PROVEX_AGE_SCHEMA`. */
218
+ schemaUid: Hex;
219
+ /** EVM chain id the attestation is bound to. */
220
+ chainId: number;
221
+ };
222
+ /**
223
+ * Build a fully-signed off-chain age + identity attestation.
224
+ *
225
+ * Unlike a release attestation (whose subject is a manifest, so its recipient
226
+ * is the zero address), the recipient here is the holder's wallet: the
227
+ * credential is ABOUT a person, and that wallet is how a consuming application
228
+ * knows whose credential it is. Gate-signed — the attester vouches that it
229
+ * derived these facts inside its trust boundary.
230
+ *
231
+ * There is deliberately no separate `expirationTime` parameter. The EAS-native
232
+ * `message.expirationTime` is DERIVED from `claim.expiresAt`, exactly as the
233
+ * identity builder derives it, so the two can never disagree. Accepting both
234
+ * would let a caller sign a credential whose envelope says "never expires"
235
+ * while its encoded data says otherwise — a consumer using standard Ethereum
236
+ * Attestation Service tooling and one decoding the claim would then reach
237
+ * opposite conclusions about the same credential, with nothing to flag it.
238
+ *
239
+ * `recipient` stays a separate parameter because {@link AgeClaim} carries no
240
+ * subject field by design — binding the holder inside the encoded data would
241
+ * put a personal identifier in the schema.
242
+ */
243
+ export declare const buildOffchainAgeAttestation: (args: BuildOffchainAgeAttestationArgs) => Promise<OffchainEasAttestation>;
244
+ /**
245
+ * Convenience wrapper: resolve `eas` + age `schemaUid` from the chainId, then
246
+ * sign. Throws if either lookup fails (chain unknown, address not pinned, or
247
+ * no age schema unique identifier pinned for that chain).
248
+ */
249
+ export declare const buildOffchainAgeAttestationForChain: (args: Omit<BuildOffchainAgeAttestationArgs, "eas" | "schemaUid">) => Promise<OffchainEasAttestation>;
250
+ /**
251
+ * Wire-shape delegated attestation signature suitable for the EAS
252
+ * contract's `attestByDelegation(DelegatedAttestationRequest)` call.
253
+ *
254
+ * Mirrors the on-chain `Signature` struct (`v, r, s`) plus the
255
+ * delegation envelope fields (`attester`, `deadline`, `nonce`) the
256
+ * contract needs to verify the signature without trusting `msg.sender`.
257
+ *
258
+ * Wire-safe: `deadline` and `nonce` are base-10 strings so the whole
259
+ * object survives `JSON.stringify` without bigint loss. Re-hydrate to
260
+ * bigint at the boundary that actually calls the contract.
261
+ *
262
+ * **Why this shape instead of the SDK's response shape:** the SDK
263
+ * returns a nested `{ message, signature, types, ... }` object intended
264
+ * for off-chain verification. We only need what `attestByDelegation`
265
+ * actually consumes — keeping the wire narrow makes the gate's
266
+ * attestationStore + the UI saved-proof both materially smaller and
267
+ * makes the contract-call site self-documenting.
268
+ */
269
+ export type DelegatedAttestationSignature = {
270
+ signature: {
271
+ v: number;
272
+ r: Hex;
273
+ s: Hex;
274
+ };
275
+ attester: Address;
276
+ /** Base-10 stringified `uint64`. `'0'` = signature never expires. */
277
+ deadline: string;
278
+ /** Base-10 stringified `uint256`. EAS contract's nonce for `attester` at sign time. */
279
+ nonce: string;
280
+ };
281
+ /**
282
+ * Inputs to {@link buildDelegatedEasAttestationRequest}.
283
+ *
284
+ * `nonce` is required because we don't connect a provider to the SDK
285
+ * (server-side signing should not depend on the EAS contract being
286
+ * reachable mid-sign). Callers fetch it via viem `getNonce(attester)`
287
+ * before signing — see `notary-gate/src/lib/delegation-signer.ts`.
288
+ *
289
+ * `deadline: 0n` is the EAS sentinel for "no expiration"; recommended
290
+ * default since the Provex publish flow already enforces an
291
+ * `expirationTime` on the attestation itself.
292
+ */
293
+ export type BuildDelegatedEasAttestationRequestArgs = {
294
+ attester: LocalAccount;
295
+ claim: IdentityClaim;
296
+ chainId: number;
297
+ eas: Address;
298
+ schemaUid: Hex;
299
+ /** EAS contract nonce for `attester.address`. Fetch via `getNonce`. */
300
+ nonce: bigint;
301
+ /** Use `0n` for no expiration (recommended). */
302
+ deadline: bigint;
303
+ /**
304
+ * EAS contract version string used in the EIP-712 domain. Must match
305
+ * the on-chain `eip712Domain().version` for `attestByDelegation` to
306
+ * verify. Same value as for off-chain attestations on the same chain.
307
+ */
308
+ contractVersion: string;
309
+ };
310
+ export declare const buildDelegatedEasAttestationRequest: (args: BuildDelegatedEasAttestationRequestArgs) => Promise<DelegatedAttestationSignature>;
311
+ /**
312
+ * Convenience wrapper: resolve `eas` + `schemaUid` from the chainId,
313
+ * pair with the canonical contract version string for the chain, then
314
+ * sign. Throws if either chain lookup fails.
315
+ *
316
+ * Prefer this over the raw {@link buildDelegatedEasAttestationRequest}
317
+ * for any caller that has a chainId — eliminates the chance of pairing
318
+ * the wrong EAS address with the wrong schema UID with the wrong
319
+ * contract version.
320
+ */
321
+ export declare const buildDelegatedEasAttestationRequestForChain: (args: Omit<BuildDelegatedEasAttestationRequestArgs, "eas" | "schemaUid" | "contractVersion"> & {
322
+ contractVersion?: string;
323
+ }) => Promise<DelegatedAttestationSignature>;
324
+ export type BuildDelegatedReleaseAttestationRequestArgs = {
325
+ attester: LocalAccount;
326
+ claim: ReleaseClaim;
327
+ chainId: number;
328
+ eas: Address;
329
+ schemaUid: Hex;
330
+ /** EAS contract nonce for `attester.address`. Fetch via `getNonce`. */
331
+ nonce: bigint;
332
+ /** Relayer submission deadline as a uint64; use `0n` for no deadline. */
333
+ deadline: bigint;
334
+ /** EAS contract version string used in the EIP-712 domain. */
335
+ contractVersion: string;
336
+ /** EAS-native freshness horizon (Unix seconds). Use `attestationExpirationTime()`. */
337
+ expirationTime: bigint;
338
+ };
339
+ /**
340
+ * Sign a delegated release attestation request — lets any caller submit
341
+ * `attestByDelegation` on-chain with the gate recorded as the attester. The
342
+ * recipient is the zero address (the subject is the manifest, not a wallet).
343
+ */
344
+ export declare const buildDelegatedReleaseAttestationRequest: (args: BuildDelegatedReleaseAttestationRequestArgs) => Promise<DelegatedAttestationSignature>;
345
+ /**
346
+ * Convenience wrapper: resolve `eas` + release `schemaUid` + the canonical
347
+ * contract version from the chainId, then sign. Throws if any lookup fails.
348
+ */
349
+ export declare const buildDelegatedReleaseAttestationRequestForChain: (args: Omit<BuildDelegatedReleaseAttestationRequestArgs, "eas" | "schemaUid" | "contractVersion"> & {
350
+ contractVersion?: string;
351
+ }) => Promise<DelegatedAttestationSignature>;
352
+ export {};