@vimine/sdk 0.2.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/dist/index.js ADDED
@@ -0,0 +1,401 @@
1
+ // @vimine/sdk — Sign in with Vimine (OAuth 2.1 + PKCE) and the member API, for websites and apps of
2
+ // associations on vimine.network. Zero dependencies; browser-first (WebCrypto, fetch), also fine in Node ≥ 18
3
+ // with a storage of your choice.
4
+ //
5
+ // const mp = new Vimine({ clientId: 'vmc_…', redirectUri: 'https://www.example.org/callback' });
6
+ // await mp.signIn(); // → the hosted sign-in + consent page, then back to redirectUri
7
+ // await mp.handleRedirect(); // on the callback page: exchanges the code, stores the session
8
+ // const membership = await mp.membership.get();
9
+ //
10
+ // The member's password is never typed into your page: sign-in happens on vimine.network, your site only ever
11
+ // holds tokens for the permissions the member granted.
12
+ import { VimineError } from './errors.js';
13
+ import { createPkce, createState } from './pkce.js';
14
+ import { DEFAULT_API_URL, eventQuery, jsonOrThrow, localeQuery, publicApi, withNetworkError } from './public.js';
15
+ import { resolveStorage } from './storage.js';
16
+ export { VimineError } from './errors.js';
17
+ export { createPkce, createState, createVerifier, createChallenge, base64url } from './pkce.js';
18
+ export { MemoryStorage } from './storage.js';
19
+ export { publicApi, DEFAULT_API_URL, eventQuery } from './public.js';
20
+ export const DEFAULT_APP_URL = 'https://app.vimine.network';
21
+ /** Refresh this long before the access token expires. */
22
+ const REFRESH_LEAD_MS = 30_000;
23
+ const decodeJwtPayload = (token) => {
24
+ try {
25
+ const part = token.split('.')[1];
26
+ if (!part)
27
+ return null;
28
+ const json = atob(part.replace(/-/g, '+').replace(/_/g, '/'));
29
+ return JSON.parse(json);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ };
35
+ export class Vimine {
36
+ options;
37
+ /** The public, credential-less endpoints (verify a card, an association's profile, fiscal code, card keys). */
38
+ public;
39
+ storage;
40
+ fetchImpl;
41
+ keys;
42
+ refreshing = null;
43
+ listeners = new Set();
44
+ constructor(options) {
45
+ if (!options?.clientId)
46
+ throw new VimineError('invalid_options', 'clientId is required');
47
+ if (!options.redirectUri)
48
+ throw new VimineError('invalid_options', 'redirectUri is required');
49
+ this.options = {
50
+ ...options,
51
+ apiUrl: (options.apiUrl || DEFAULT_API_URL).replace(/\/+$/, ''),
52
+ appUrl: (options.appUrl || DEFAULT_APP_URL).replace(/\/+$/, ''),
53
+ locale: options.locale || 'it'
54
+ };
55
+ this.storage = resolveStorage(options.storage);
56
+ this.fetchImpl = options.fetch || globalThis.fetch.bind(globalThis);
57
+ const prefix = options.storageKey || `vimine:${options.clientId}:`;
58
+ this.keys = { session: `${prefix}session`, flow: `${prefix}flow` };
59
+ this.public = publicApi({ apiUrl: this.options.apiUrl, fetch: this.fetchImpl });
60
+ // Sign-out (or sign-in) in another tab, when tokens live in localStorage.
61
+ const w = globalThis;
62
+ if (options.storage === 'local' && typeof w.addEventListener === 'function') {
63
+ w.addEventListener('storage', (e) => {
64
+ if (e.key === this.keys.session)
65
+ this.emit(this.getSession());
66
+ });
67
+ }
68
+ }
69
+ // ---- sign-in -----------------------------------------------------------------------------------
70
+ /** Build the authorization URL and remember the pending flow. Use `signIn()` unless you render the link yourself. */
71
+ async createSignInUrl(opts = {}) {
72
+ const { verifier, challenge } = await createPkce();
73
+ const state = createState();
74
+ const scopes = opts.scopes || this.options.scopes || ['profile:read', 'membership:read'];
75
+ const flow = { verifier, state, redirectUri: this.options.redirectUri, scope: scopes.join(' '), returnTo: opts.returnTo ?? null, createdAt: Date.now() };
76
+ this.storage.set(this.keys.flow, JSON.stringify(flow));
77
+ const u = new URL(`${this.options.appUrl}/authorize`);
78
+ u.searchParams.set('client_id', this.options.clientId);
79
+ u.searchParams.set('redirect_uri', this.options.redirectUri);
80
+ u.searchParams.set('response_type', 'code');
81
+ u.searchParams.set('state', state);
82
+ u.searchParams.set('code_challenge', challenge);
83
+ u.searchParams.set('code_challenge_method', 'S256');
84
+ u.searchParams.set('scope', flow.scope);
85
+ u.searchParams.set('ui_locales', this.options.locale);
86
+ if (opts.prompt)
87
+ u.searchParams.set('prompt', opts.prompt);
88
+ return u.toString();
89
+ }
90
+ /**
91
+ * Start the sign-in: navigates the TOP window to the hosted sign-in + consent page. Call it from a user
92
+ * gesture (a click). From inside a cross-origin or sandboxed frame the navigation is refused (`framed`):
93
+ * the hosted page is never shown in a frame.
94
+ */
95
+ async signIn(opts = {}) {
96
+ const url = await this.createSignInUrl(opts);
97
+ const nav = this.options.navigate || defaultNavigate;
98
+ try {
99
+ nav(url);
100
+ }
101
+ catch (err) {
102
+ this.storage.remove(this.keys.flow);
103
+ throw new VimineError('framed', 'Sign in with Vimine cannot start from this frame', { cause: err });
104
+ }
105
+ }
106
+ /** Does this URL carry the parameters of a sign-in redirect (`code`+`state` or `error`)? */
107
+ hasRedirectParams(url) {
108
+ const params = paramsOf(url);
109
+ return !!params && ((params.has('code') && params.has('state')) || params.has('error'));
110
+ }
111
+ /**
112
+ * On the redirect page: exchange the authorization code for tokens and store the session. Returns null when
113
+ * the URL carries no sign-in parameters. Throws `state_mismatch` / `no_flow` for a redirect this client did
114
+ * not start, and the OAuth error (`access_denied`, …) when the member did not authorize.
115
+ */
116
+ async handleRedirect(opts = {}) {
117
+ const params = paramsOf(opts.url);
118
+ if (!params || !this.hasRedirectParams(opts.url))
119
+ return null;
120
+ const raw = this.storage.get(this.keys.flow);
121
+ const flow = raw ? JSON.parse(raw) : null;
122
+ const finish = () => {
123
+ this.storage.remove(this.keys.flow);
124
+ if (opts.cleanUrl !== false)
125
+ cleanRedirectParams();
126
+ };
127
+ if (params.has('error')) {
128
+ finish();
129
+ throw new VimineError(params.get('error') || 'access_denied', params.get('error_description') || 'The member did not authorize this app');
130
+ }
131
+ if (!flow)
132
+ throw new VimineError('no_flow', 'No sign-in was started from this browser (storage empty or expired)');
133
+ if (params.get('state') !== flow.state)
134
+ throw new VimineError('state_mismatch', 'The redirect does not match the sign-in that was started');
135
+ const tokens = await this.tokenRequest({
136
+ grant_type: 'authorization_code',
137
+ code: params.get('code') || '',
138
+ code_verifier: flow.verifier,
139
+ client_id: this.options.clientId,
140
+ redirect_uri: flow.redirectUri
141
+ });
142
+ finish();
143
+ const session = this.storeTokens(tokens);
144
+ return { session, returnTo: flow.returnTo };
145
+ }
146
+ // ---- session -----------------------------------------------------------------------------------
147
+ getSession() {
148
+ const raw = this.storage.get(this.keys.session);
149
+ if (!raw)
150
+ return null;
151
+ try {
152
+ return JSON.parse(raw);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ isSignedIn() {
159
+ return !!this.getSession();
160
+ }
161
+ /** A valid access token, refreshed when about to expire. Throws `not_signed_in` when there is no session. */
162
+ async getAccessToken() {
163
+ const session = this.getSession();
164
+ if (!session)
165
+ throw new VimineError('not_signed_in', 'No Vimine session: call signIn() first');
166
+ if (session.expiresAt - Date.now() > REFRESH_LEAD_MS)
167
+ return session.accessToken;
168
+ return (await this.refresh()).accessToken;
169
+ }
170
+ /** Rotate the session (single-flight). An unusable refresh token ends the session. */
171
+ refresh() {
172
+ if (this.refreshing)
173
+ return this.refreshing;
174
+ this.refreshing = (async () => {
175
+ const session = this.getSession();
176
+ if (!session?.refreshToken) {
177
+ this.clearSession();
178
+ throw new VimineError('not_signed_in', 'No refresh token: sign in again');
179
+ }
180
+ try {
181
+ const tokens = await this.tokenRequest({ grant_type: 'refresh_token', refresh_token: session.refreshToken, client_id: this.options.clientId });
182
+ return this.storeTokens(tokens);
183
+ }
184
+ catch (err) {
185
+ if (err instanceof VimineError && err.isAuthError)
186
+ this.clearSession();
187
+ throw err;
188
+ }
189
+ finally {
190
+ this.refreshing = null;
191
+ }
192
+ })();
193
+ return this.refreshing;
194
+ }
195
+ /** Revoke the session (RFC 7009) and forget it; with `everywhere`, also end the member's Vimine session. */
196
+ async signOut(opts = {}) {
197
+ const session = this.getSession();
198
+ this.clearSession();
199
+ if (session?.refreshToken) {
200
+ await withNetworkError(this.fetchImpl(`${this.options.apiUrl}/v1/oauth/revoke`, {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
203
+ body: new URLSearchParams({ token: session.refreshToken, token_type_hint: 'refresh_token' }).toString()
204
+ })).catch(() => {
205
+ /* revocation is best effort: the tokens are already forgotten here */
206
+ });
207
+ }
208
+ if (opts.everywhere) {
209
+ const u = new URL(`${this.options.appUrl}/oauth/logout`);
210
+ u.searchParams.set('client_id', this.options.clientId);
211
+ u.searchParams.set('post_logout_redirect_uri', opts.postLogoutRedirectUri || this.options.redirectUri);
212
+ if (opts.state)
213
+ u.searchParams.set('state', opts.state);
214
+ (this.options.navigate || defaultNavigate)(u.toString());
215
+ }
216
+ }
217
+ /** Subscribe to session changes (sign-in, refresh, sign-out; other tabs when storage is `local`). */
218
+ onChange(listener) {
219
+ this.listeners.add(listener);
220
+ return () => this.listeners.delete(listener);
221
+ }
222
+ // ---- API ---------------------------------------------------------------------------------------
223
+ /** An authenticated call to the API; JSON in and out, problem details as `VimineError`, one retry after a refresh on 401. */
224
+ async request(path, init = {}) {
225
+ const attempt = async (token) => {
226
+ const headers = { Accept: 'application/json', Authorization: `Bearer ${token}` };
227
+ if (init.body !== undefined)
228
+ headers['Content-Type'] = 'application/json';
229
+ if (init.idempotencyKey)
230
+ headers['Idempotency-Key'] = init.idempotencyKey;
231
+ const given = init.headers;
232
+ const { idempotencyKey: _ignored, ...rest } = init;
233
+ return this.fetchImpl(`${this.options.apiUrl}${path}`, { ...rest, headers: { ...headers, ...(given || {}) } });
234
+ };
235
+ let res = await withNetworkError(attempt(await this.getAccessToken()));
236
+ if (res.status === 401) {
237
+ const refreshed = await this.refresh();
238
+ res = await withNetworkError(attempt(refreshed.accessToken));
239
+ }
240
+ return jsonOrThrow(res);
241
+ }
242
+ /** The member's profile (`profile:read` / `profile:write`). */
243
+ me = {
244
+ get: () => this.request('/v1/me'),
245
+ /**
246
+ * A fiscal code that disagrees with the particulars answers 422 with a `codicefiscale_mismatch` entry in
247
+ * `errors[]`; when that entry says `confirmable: true` the person may confirm it: send `codicefiscale_confirmed: true`.
248
+ */
249
+ update: (patch) => this.request('/v1/me', { method: 'PATCH', body: JSON.stringify(patch) })
250
+ };
251
+ /** The member's membership in this association (`membership:read`, `membership:request`, `statuto:accept`). */
252
+ membership = {
253
+ get: () => this.request('/v1/me/membership'),
254
+ /**
255
+ * Ask for a membership. 422 `identification_required` when the profile is not identified yet — a fiscal code, or
256
+ * date, place and country of birth: `errors[]` names what is missing; complete it with `me.update()` and ask again.
257
+ */
258
+ request: (body, opts = {}) => this.request('/v1/me/membership', { method: 'POST', body: JSON.stringify(body), idempotencyKey: opts.idempotencyKey || createState() }),
259
+ events: (params = {}) => {
260
+ const q = new URLSearchParams();
261
+ if (params.limit)
262
+ q.set('limit', String(params.limit));
263
+ if (params.cursor)
264
+ q.set('cursor', params.cursor);
265
+ const qs = q.toString();
266
+ return this.request(`/v1/me/membership/events${qs ? `?${qs}` : ''}`);
267
+ },
268
+ statuto: () => this.request('/v1/me/membership/statuto'),
269
+ acceptStatuto: (body) => this.request('/v1/me/membership/statuto/accept', { method: 'POST', body: JSON.stringify(body) }),
270
+ /** The signed card token and its window. 409 `not_active` unless the membership is active: only an active membership has a card. */
271
+ card: () => this.request('/v1/me/membership/card'),
272
+ /** Self-service renewal (`membership:request`): renewed at once for a free type with no approval gate, else a request. */
273
+ renew: (body, opts = {}) => this.request('/v1/me/membership/renew', { method: 'POST', body: JSON.stringify({ accept_statuto: body }), idempotencyKey: opts.idempotencyKey || createState() }),
274
+ /** Network consent (`network:manage`); 409 `not_eligible` when the membership type is not in the network. */
275
+ setNetworkOptIn: (optIn) => this.request('/v1/me/membership/network-opt-in', { method: 'PATCH', body: JSON.stringify({ opt_in: optIn }) }),
276
+ /** A Save-to-Google-Wallet link for the card (`membership:read`); 409 `not_active` when there is no card yet, 503 when the deployment has no wallet issuer. */
277
+ googleWalletUrl: () => this.request('/v1/me/membership/wallet/google'),
278
+ /** The card as SVG markup (with the current QR) — inline it, or hand it to `<vimine-card>`. 409 `not_active` unless the membership is active. */
279
+ cardSvg: async () => {
280
+ const token = await this.getAccessToken();
281
+ const res = await withNetworkError(this.fetchImpl(`${this.options.apiUrl}/v1/me/membership/card.svg`, { headers: { Accept: 'image/svg+xml', Authorization: `Bearer ${token}` } }));
282
+ if (!res.ok)
283
+ throw await VimineError.fromResponse(res);
284
+ return res.text();
285
+ },
286
+ /** Ask the API to e-mail the member their card (SVG + QR attached). 409 `not_active` unless the membership is active. */
287
+ emailCard: () => this.request('/v1/me/membership/card/email', { method: 'POST' })
288
+ };
289
+ /**
290
+ * The member's legal acceptances (`legal:read`, `legal:accept`): what they accepted or acknowledged, what is still
291
+ * pending — the account terms, the platform privacy notice, this association's notice to members — and the call
292
+ * that records one at the in-force version your app showed (422 `<kind>_outdated` otherwise).
293
+ */
294
+ legal = {
295
+ acceptances: (params = {}) => this.request(`/v1/me/acceptances${params.locale ? `?locale=${params.locale}` : ''}`),
296
+ record: (body, opts = {}) => this.request('/v1/me/acceptances', { method: 'POST', body: JSON.stringify(body), idempotencyKey: opts.idempotencyKey || createState() })
297
+ };
298
+ /** Events of the association (`events:read`): published ones, members-only included — the public ones need no sign-in: `publicApi().events`. */
299
+ events = {
300
+ /** `filter`: upcoming (default) | past | cancelled | all; `from` / `to` bound `starts_at`. */
301
+ list: (params = {}) => this.request(`/v1/events${eventQuery(params)}`),
302
+ get: (slug, params = {}) => this.request(`/v1/events/${encodeURIComponent(slug)}${localeQuery(params)}`),
303
+ /** May the member register, per registration type, with reasons. */
304
+ eligibility: (slug) => this.request(`/v1/events/${encodeURIComponent(slug)}/eligibility`)
305
+ };
306
+ /** The member's event registrations (`registrations:read` / `registrations:write`). */
307
+ registrations = {
308
+ /**
309
+ * Register for an event: a free type answers `confirmed` with the QR `token`; a paid one answers `reserved`
310
+ * with `payment_instructions` until the association records the contribution. `422 not_eligible` carries
311
+ * the reasons. Retries are safe: an `Idempotency-Key` is generated unless you pass one.
312
+ */
313
+ create: (slug, body, opts = {}) => this.request(`/v1/events/${encodeURIComponent(slug)}/registrations`, { method: 'POST', body: JSON.stringify(body), idempotencyKey: opts.idempotencyKey || createState() }),
314
+ /** The member's registrations, newest event first (`filter: 'upcoming'` for the ones still to come). */
315
+ list: (params = {}) => this.request(`/v1/me/registrations${params.filter ? `?filter=${params.filter}` : ''}`),
316
+ get: (id) => this.request(`/v1/me/registrations/${encodeURIComponent(id)}`),
317
+ /** Cancel inside the event's cancellation window — `409 window_closed` otherwise. */
318
+ cancel: (id) => this.request(`/v1/me/registrations/${encodeURIComponent(id)}`, { method: 'DELETE' }),
319
+ /** The QR payload of a confirmed registration (what the door scans), or null. */
320
+ tokenPayload: async (id) => (await this.request(`/v1/me/registrations/${encodeURIComponent(id)}`)).token || null
321
+ };
322
+ /** The apps the member authorized in this association. */
323
+ connections = {
324
+ list: () => this.request('/v1/me/connections'),
325
+ revoke: (clientId) => this.request(`/v1/me/connections/${encodeURIComponent(clientId)}`, { method: 'DELETE' })
326
+ };
327
+ // ---- internals ---------------------------------------------------------------------------------
328
+ async tokenRequest(form) {
329
+ const res = await withNetworkError(this.fetchImpl(`${this.options.apiUrl}/v1/oauth/token`, {
330
+ method: 'POST',
331
+ headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
332
+ body: new URLSearchParams(form).toString()
333
+ }));
334
+ return jsonOrThrow(res);
335
+ }
336
+ storeTokens(tokens) {
337
+ const claims = decodeJwtPayload(tokens.access_token);
338
+ const session = {
339
+ accessToken: tokens.access_token,
340
+ refreshToken: tokens.refresh_token ?? this.getSession()?.refreshToken ?? null,
341
+ expiresAt: Date.now() + Math.max(0, tokens.expires_in) * 1000,
342
+ scope: String(tokens.scope || '').split(/\s+/).filter(Boolean),
343
+ sub: tokens.sub ?? (typeof claims?.sub === 'string' ? claims.sub : null),
344
+ mode: claims?.mode === 'test' ? 'test' : 'live'
345
+ };
346
+ this.storage.set(this.keys.session, JSON.stringify(session));
347
+ this.emit(session);
348
+ return session;
349
+ }
350
+ clearSession() {
351
+ const had = !!this.storage.get(this.keys.session);
352
+ this.storage.remove(this.keys.session);
353
+ if (had)
354
+ this.emit(null);
355
+ }
356
+ emit(session) {
357
+ for (const l of this.listeners) {
358
+ try {
359
+ l(session);
360
+ }
361
+ catch {
362
+ /* a listener must not break the others */
363
+ }
364
+ }
365
+ }
366
+ }
367
+ const paramsOf = (url) => {
368
+ if (url) {
369
+ try {
370
+ return new URL(url, 'http://localhost').searchParams;
371
+ }
372
+ catch {
373
+ return null;
374
+ }
375
+ }
376
+ const loc = globalThis.location;
377
+ return loc ? new URLSearchParams(loc.search) : null;
378
+ };
379
+ const cleanRedirectParams = () => {
380
+ const g = globalThis;
381
+ if (!g.history || !g.location)
382
+ return;
383
+ const u = new URL(g.location.href);
384
+ for (const k of ['code', 'state', 'error', 'error_description'])
385
+ u.searchParams.delete(k);
386
+ try {
387
+ g.history.replaceState(g.history.state, '', u.toString());
388
+ }
389
+ catch {
390
+ /* ignore */
391
+ }
392
+ };
393
+ const defaultNavigate = (url) => {
394
+ const w = globalThis;
395
+ const top = w.top || w.self;
396
+ if (!top)
397
+ throw new Error('No window to navigate');
398
+ // Navigating the top window from a cross-origin frame throws — which is the refusal we want.
399
+ top.location.assign(url);
400
+ };
401
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["// @vimine/sdk — Sign in with Vimine (OAuth 2.1 + PKCE) and the member API, for websites and apps of\n// associations on vimine.network. Zero dependencies; browser-first (WebCrypto, fetch), also fine in Node ≥ 18\n// with a storage of your choice.\n//\n// const mp = new Vimine({ clientId: 'vmc_…', redirectUri: 'https://www.example.org/callback' });\n// await mp.signIn(); // → the hosted sign-in + consent page, then back to redirectUri\n// await mp.handleRedirect(); // on the callback page: exchanges the code, stores the session\n// const membership = await mp.membership.get();\n//\n// The member's password is never typed into your page: sign-in happens on vimine.network, your site only ever\n// holds tokens for the permissions the member granted.\nimport { VimineError } from './errors.js';\nimport { createPkce, createState } from './pkce.js';\nimport { DEFAULT_API_URL, eventQuery, jsonOrThrow, localeQuery, publicApi, withNetworkError, type PublicApi } from './public.js';\nimport { resolveStorage, type StorageKind, type TokenStorage } from './storage.js';\nimport type {\n AcceptanceInput,\n AcceptanceResult,\n Accepted,\n Card,\n Connection,\n Eligibility,\n Event,\n EventList,\n EventListParams,\n LocaleParam,\n Me,\n Membership,\n MembershipEvents,\n MembershipRequest,\n MembershipRequestResult,\n MeUpdate,\n MyAcceptances,\n NetworkOptIn,\n Registration,\n RegistrationList,\n RegistrationRequest,\n RenewResult,\n Scope,\n StatutoAccept,\n StatutoInfo,\n TokenResponse\n} from './types.js';\n\nexport { VimineError } from './errors.js';\nexport type { FieldError, ProblemDetails } from './errors.js';\nexport { createPkce, createState, createVerifier, createChallenge, base64url } from './pkce.js';\nexport { MemoryStorage, type StorageKind, type TokenStorage } from './storage.js';\n/** The OpenAPI document's own vocabulary (generated from openapi/v1.json — `npm run spec:types`). */\nexport type * as Spec from './generated.js';\nexport { publicApi, DEFAULT_API_URL, eventQuery, type PublicApi, type PublicOptions } from './public.js';\nexport type * from './types.js';\n\nexport const DEFAULT_APP_URL = 'https://app.vimine.network';\n/** Refresh this long before the access token expires. */\nconst REFRESH_LEAD_MS = 30_000;\n\nexport interface VimineOptions {\n /** Your client id (`vmc_…`), from the association's developer console. */\n clientId: string;\n /** One of the redirect URIs registered for the client — the page that calls `handleRedirect()`. */\n redirectUri: string;\n /** Default permissions to ask for; `signIn()` may override. */\n scopes?: Scope[];\n /** The API origin (default production). */\n apiUrl?: string;\n /** The app origin that hosts sign-in and consent (default production). */\n appUrl?: string;\n /** Where tokens live: `session` (default, per tab), `local` (shared across tabs), `memory`, or your own adapter. */\n storage?: StorageKind | TokenStorage;\n /** Storage key prefix (default derived from the client id). */\n storageKey?: string;\n /** UI language of the hosted pages. */\n locale?: 'it' | 'en';\n /** Custom fetch (tests, polyfills). */\n fetch?: typeof fetch;\n /** How to leave the page (default: the top window's `location.assign`). */\n navigate?: (url: string) => void;\n}\n\nexport interface SignInOptions {\n scopes?: Scope[];\n /** `login` forces the credential form even with a live session; `consent` forces the consent screen. */\n prompt?: 'login' | 'consent';\n /** Your own state to get back after the redirect (kept in storage, not sent). */\n returnTo?: string;\n}\n\nexport interface Session {\n accessToken: string;\n refreshToken: string | null;\n /** Absolute time (ms) the access token expires. */\n expiresAt: number;\n scope: Scope[];\n /** The member's pairwise subject for this association. */\n sub: string | null;\n /** `test` for sessions minted through a sandbox client. */\n mode?: 'live' | 'test';\n}\n\nexport interface RedirectResult {\n session: Session;\n /** The `returnTo` given to `signIn()`, if any. */\n returnTo: string | null;\n}\n\nexport interface SignOutOptions {\n /** Also end the member's Vimine session (single sign-out): leaves the page for the app's logout route. */\n everywhere?: boolean;\n /** Where the app sends the member afterwards (must be on a registered origin); default: `redirectUri`. */\n postLogoutRedirectUri?: string;\n state?: string;\n}\n\ninterface PendingFlow {\n verifier: string;\n state: string;\n redirectUri: string;\n scope: string;\n returnTo: string | null;\n createdAt: number;\n}\n\nconst decodeJwtPayload = (token: string): Record<string, unknown> | null => {\n try {\n const part = token.split('.')[1];\n if (!part) return null;\n const json = atob(part.replace(/-/g, '+').replace(/_/g, '/'));\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n};\n\nexport class Vimine {\n readonly options: Required<Pick<VimineOptions, 'clientId' | 'redirectUri' | 'apiUrl' | 'appUrl' | 'locale'>> & VimineOptions;\n /** The public, credential-less endpoints (verify a card, an association's profile, fiscal code, card keys). */\n readonly public: PublicApi;\n private readonly storage: TokenStorage;\n private readonly fetchImpl: typeof fetch;\n private readonly keys: { session: string; flow: string };\n private refreshing: Promise<Session> | null = null;\n private readonly listeners = new Set<(session: Session | null) => void>();\n\n constructor(options: VimineOptions) {\n if (!options?.clientId) throw new VimineError('invalid_options', 'clientId is required');\n if (!options.redirectUri) throw new VimineError('invalid_options', 'redirectUri is required');\n this.options = {\n ...options,\n apiUrl: (options.apiUrl || DEFAULT_API_URL).replace(/\\/+$/, ''),\n appUrl: (options.appUrl || DEFAULT_APP_URL).replace(/\\/+$/, ''),\n locale: options.locale || 'it'\n };\n this.storage = resolveStorage(options.storage);\n this.fetchImpl = options.fetch || globalThis.fetch.bind(globalThis);\n const prefix = options.storageKey || `vimine:${options.clientId}:`;\n this.keys = { session: `${prefix}session`, flow: `${prefix}flow` };\n this.public = publicApi({ apiUrl: this.options.apiUrl, fetch: this.fetchImpl });\n // Sign-out (or sign-in) in another tab, when tokens live in localStorage.\n const w = globalThis as unknown as { addEventListener?: (t: string, l: (e: StorageEvent) => void) => void };\n if (options.storage === 'local' && typeof w.addEventListener === 'function') {\n w.addEventListener('storage', (e) => {\n if (e.key === this.keys.session) this.emit(this.getSession());\n });\n }\n }\n\n // ---- sign-in -----------------------------------------------------------------------------------\n\n /** Build the authorization URL and remember the pending flow. Use `signIn()` unless you render the link yourself. */\n async createSignInUrl(opts: SignInOptions = {}): Promise<string> {\n const { verifier, challenge } = await createPkce();\n const state = createState();\n const scopes = opts.scopes || this.options.scopes || ['profile:read', 'membership:read'];\n const flow: PendingFlow = { verifier, state, redirectUri: this.options.redirectUri, scope: scopes.join(' '), returnTo: opts.returnTo ?? null, createdAt: Date.now() };\n this.storage.set(this.keys.flow, JSON.stringify(flow));\n const u = new URL(`${this.options.appUrl}/authorize`);\n u.searchParams.set('client_id', this.options.clientId);\n u.searchParams.set('redirect_uri', this.options.redirectUri);\n u.searchParams.set('response_type', 'code');\n u.searchParams.set('state', state);\n u.searchParams.set('code_challenge', challenge);\n u.searchParams.set('code_challenge_method', 'S256');\n u.searchParams.set('scope', flow.scope);\n u.searchParams.set('ui_locales', this.options.locale);\n if (opts.prompt) u.searchParams.set('prompt', opts.prompt);\n return u.toString();\n }\n\n /**\n * Start the sign-in: navigates the TOP window to the hosted sign-in + consent page. Call it from a user\n * gesture (a click). From inside a cross-origin or sandboxed frame the navigation is refused (`framed`):\n * the hosted page is never shown in a frame.\n */\n async signIn(opts: SignInOptions = {}): Promise<void> {\n const url = await this.createSignInUrl(opts);\n const nav = this.options.navigate || defaultNavigate;\n try {\n nav(url);\n } catch (err) {\n this.storage.remove(this.keys.flow);\n throw new VimineError('framed', 'Sign in with Vimine cannot start from this frame', { cause: err });\n }\n }\n\n /** Does this URL carry the parameters of a sign-in redirect (`code`+`state` or `error`)? */\n hasRedirectParams(url?: string): boolean {\n const params = paramsOf(url);\n return !!params && ((params.has('code') && params.has('state')) || params.has('error'));\n }\n\n /**\n * On the redirect page: exchange the authorization code for tokens and store the session. Returns null when\n * the URL carries no sign-in parameters. Throws `state_mismatch` / `no_flow` for a redirect this client did\n * not start, and the OAuth error (`access_denied`, …) when the member did not authorize.\n */\n async handleRedirect(opts: { url?: string; cleanUrl?: boolean } = {}): Promise<RedirectResult | null> {\n const params = paramsOf(opts.url);\n if (!params || !this.hasRedirectParams(opts.url)) return null;\n const raw = this.storage.get(this.keys.flow);\n const flow: PendingFlow | null = raw ? (JSON.parse(raw) as PendingFlow) : null;\n const finish = () => {\n this.storage.remove(this.keys.flow);\n if (opts.cleanUrl !== false) cleanRedirectParams();\n };\n if (params.has('error')) {\n finish();\n throw new VimineError(params.get('error') || 'access_denied', params.get('error_description') || 'The member did not authorize this app');\n }\n if (!flow) throw new VimineError('no_flow', 'No sign-in was started from this browser (storage empty or expired)');\n if (params.get('state') !== flow.state) throw new VimineError('state_mismatch', 'The redirect does not match the sign-in that was started');\n const tokens = await this.tokenRequest({\n grant_type: 'authorization_code',\n code: params.get('code') || '',\n code_verifier: flow.verifier,\n client_id: this.options.clientId,\n redirect_uri: flow.redirectUri\n });\n finish();\n const session = this.storeTokens(tokens);\n return { session, returnTo: flow.returnTo };\n }\n\n // ---- session -----------------------------------------------------------------------------------\n\n getSession(): Session | null {\n const raw = this.storage.get(this.keys.session);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as Session;\n } catch {\n return null;\n }\n }\n\n isSignedIn(): boolean {\n return !!this.getSession();\n }\n\n /** A valid access token, refreshed when about to expire. Throws `not_signed_in` when there is no session. */\n async getAccessToken(): Promise<string> {\n const session = this.getSession();\n if (!session) throw new VimineError('not_signed_in', 'No Vimine session: call signIn() first');\n if (session.expiresAt - Date.now() > REFRESH_LEAD_MS) return session.accessToken;\n return (await this.refresh()).accessToken;\n }\n\n /** Rotate the session (single-flight). An unusable refresh token ends the session. */\n refresh(): Promise<Session> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = (async () => {\n const session = this.getSession();\n if (!session?.refreshToken) {\n this.clearSession();\n throw new VimineError('not_signed_in', 'No refresh token: sign in again');\n }\n try {\n const tokens = await this.tokenRequest({ grant_type: 'refresh_token', refresh_token: session.refreshToken, client_id: this.options.clientId });\n return this.storeTokens(tokens);\n } catch (err) {\n if (err instanceof VimineError && err.isAuthError) this.clearSession();\n throw err;\n } finally {\n this.refreshing = null;\n }\n })();\n return this.refreshing;\n }\n\n /** Revoke the session (RFC 7009) and forget it; with `everywhere`, also end the member's Vimine session. */\n async signOut(opts: SignOutOptions = {}): Promise<void> {\n const session = this.getSession();\n this.clearSession();\n if (session?.refreshToken) {\n await withNetworkError(\n this.fetchImpl(`${this.options.apiUrl}/v1/oauth/revoke`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ token: session.refreshToken, token_type_hint: 'refresh_token' }).toString()\n })\n ).catch(() => {\n /* revocation is best effort: the tokens are already forgotten here */\n });\n }\n if (opts.everywhere) {\n const u = new URL(`${this.options.appUrl}/oauth/logout`);\n u.searchParams.set('client_id', this.options.clientId);\n u.searchParams.set('post_logout_redirect_uri', opts.postLogoutRedirectUri || this.options.redirectUri);\n if (opts.state) u.searchParams.set('state', opts.state);\n (this.options.navigate || defaultNavigate)(u.toString());\n }\n }\n\n /** Subscribe to session changes (sign-in, refresh, sign-out; other tabs when storage is `local`). */\n onChange(listener: (session: Session | null) => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n // ---- API ---------------------------------------------------------------------------------------\n\n /** An authenticated call to the API; JSON in and out, problem details as `VimineError`, one retry after a refresh on 401. */\n async request<T>(path: string, init: RequestInit & { idempotencyKey?: string } = {}): Promise<T> {\n const attempt = async (token: string): Promise<Response> => {\n const headers: Record<string, string> = { Accept: 'application/json', Authorization: `Bearer ${token}` };\n if (init.body !== undefined) headers['Content-Type'] = 'application/json';\n if (init.idempotencyKey) headers['Idempotency-Key'] = init.idempotencyKey;\n const given = init.headers as Record<string, string> | undefined;\n const { idempotencyKey: _ignored, ...rest } = init;\n return this.fetchImpl(`${this.options.apiUrl}${path}`, { ...rest, headers: { ...headers, ...(given || {}) } });\n };\n let res = await withNetworkError(attempt(await this.getAccessToken()));\n if (res.status === 401) {\n const refreshed = await this.refresh();\n res = await withNetworkError(attempt(refreshed.accessToken));\n }\n return jsonOrThrow<T>(res);\n }\n\n /** The member's profile (`profile:read` / `profile:write`). */\n readonly me = {\n get: (): Promise<Me> => this.request<Me>('/v1/me'),\n /**\n * A fiscal code that disagrees with the particulars answers 422 with a `codicefiscale_mismatch` entry in\n * `errors[]`; when that entry says `confirmable: true` the person may confirm it: send `codicefiscale_confirmed: true`.\n */\n update: (patch: MeUpdate): Promise<Me> => this.request<Me>('/v1/me', { method: 'PATCH', body: JSON.stringify(patch) })\n };\n\n /** The member's membership in this association (`membership:read`, `membership:request`, `statuto:accept`). */\n readonly membership = {\n get: (): Promise<Membership> => this.request<Membership>('/v1/me/membership'),\n /**\n * Ask for a membership. 422 `identification_required` when the profile is not identified yet — a fiscal code, or\n * date, place and country of birth: `errors[]` names what is missing; complete it with `me.update()` and ask again.\n */\n request: (body: MembershipRequest, opts: { idempotencyKey?: string } = {}): Promise<MembershipRequestResult> =>\n this.request<MembershipRequestResult>('/v1/me/membership', { method: 'POST', body: JSON.stringify(body), idempotencyKey: opts.idempotencyKey || createState() }),\n events: (params: { limit?: number; cursor?: string } = {}): Promise<MembershipEvents> => {\n const q = new URLSearchParams();\n if (params.limit) q.set('limit', String(params.limit));\n if (params.cursor) q.set('cursor', params.cursor);\n const qs = q.toString();\n return this.request<MembershipEvents>(`/v1/me/membership/events${qs ? `?${qs}` : ''}`);\n },\n statuto: (): Promise<StatutoInfo> => this.request<StatutoInfo>('/v1/me/membership/statuto'),\n acceptStatuto: (body: StatutoAccept): Promise<{ accepted: true; version: number }> =>\n this.request('/v1/me/membership/statuto/accept', { method: 'POST', body: JSON.stringify(body) }),\n /** The signed card token and its window. 409 `not_active` unless the membership is active: only an active membership has a card. */\n card: (): Promise<Card> => this.request<Card>('/v1/me/membership/card'),\n /** Self-service renewal (`membership:request`): renewed at once for a free type with no approval gate, else a request. */\n renew: (body: StatutoAccept, opts: { idempotencyKey?: string } = {}): Promise<RenewResult> =>\n this.request<RenewResult>('/v1/me/membership/renew', { method: 'POST', body: JSON.stringify({ accept_statuto: body }), idempotencyKey: opts.idempotencyKey || createState() }),\n /** Network consent (`network:manage`); 409 `not_eligible` when the membership type is not in the network. */\n setNetworkOptIn: (optIn: boolean): Promise<NetworkOptIn> => this.request<NetworkOptIn>('/v1/me/membership/network-opt-in', { method: 'PATCH', body: JSON.stringify({ opt_in: optIn }) }),\n /** A Save-to-Google-Wallet link for the card (`membership:read`); 409 `not_active` when there is no card yet, 503 when the deployment has no wallet issuer. */\n googleWalletUrl: (): Promise<{ url: string }> => this.request<{ url: string }>('/v1/me/membership/wallet/google'),\n /** The card as SVG markup (with the current QR) — inline it, or hand it to `<vimine-card>`. 409 `not_active` unless the membership is active. */\n cardSvg: async (): Promise<string> => {\n const token = await this.getAccessToken();\n const res = await withNetworkError(this.fetchImpl(`${this.options.apiUrl}/v1/me/membership/card.svg`, { headers: { Accept: 'image/svg+xml', Authorization: `Bearer ${token}` } }));\n if (!res.ok) throw await VimineError.fromResponse(res);\n return res.text();\n },\n /** Ask the API to e-mail the member their card (SVG + QR attached). 409 `not_active` unless the membership is active. */\n emailCard: (): Promise<Accepted> => this.request<Accepted>('/v1/me/membership/card/email', { method: 'POST' })\n };\n\n /**\n * The member's legal acceptances (`legal:read`, `legal:accept`): what they accepted or acknowledged, what is still\n * pending — the account terms, the platform privacy notice, this association's notice to members — and the call\n * that records one at the in-force version your app showed (422 `<kind>_outdated` otherwise).\n */\n readonly legal = {\n acceptances: (params: { locale?: 'it' | 'en' } = {}): Promise<MyAcceptances> => this.request<MyAcceptances>(`/v1/me/acceptances${params.locale ? `?locale=${params.locale}` : ''}`),\n record: (body: AcceptanceInput, opts: { idempotencyKey?: string } = {}): Promise<AcceptanceResult> =>\n this.request<AcceptanceResult>('/v1/me/acceptances', { method: 'POST', body: JSON.stringify(body), idempotencyKey: opts.idempotencyKey || createState() })\n };\n\n /** Events of the association (`events:read`): published ones, members-only included — the public ones need no sign-in: `publicApi().events`. */\n readonly events = {\n /** `filter`: upcoming (default) | past | cancelled | all; `from` / `to` bound `starts_at`. */\n list: (params: EventListParams = {}): Promise<EventList> => this.request<EventList>(`/v1/events${eventQuery(params)}`),\n get: (slug: string, params: LocaleParam = {}): Promise<Event> => this.request<Event>(`/v1/events/${encodeURIComponent(slug)}${localeQuery(params)}`),\n /** May the member register, per registration type, with reasons. */\n eligibility: (slug: string): Promise<Eligibility> => this.request<Eligibility>(`/v1/events/${encodeURIComponent(slug)}/eligibility`)\n };\n\n /** The member's event registrations (`registrations:read` / `registrations:write`). */\n readonly registrations = {\n /**\n * Register for an event: a free type answers `confirmed` with the QR `token`; a paid one answers `reserved`\n * with `payment_instructions` until the association records the contribution. `422 not_eligible` carries\n * the reasons. Retries are safe: an `Idempotency-Key` is generated unless you pass one.\n */\n create: (slug: string, body: RegistrationRequest, opts: { idempotencyKey?: string } = {}): Promise<Registration> =>\n this.request<Registration>(`/v1/events/${encodeURIComponent(slug)}/registrations`, { method: 'POST', body: JSON.stringify(body), idempotencyKey: opts.idempotencyKey || createState() }),\n /** The member's registrations, newest event first (`filter: 'upcoming'` for the ones still to come). */\n list: (params: { filter?: 'upcoming' | 'all' } = {}): Promise<RegistrationList> => this.request<RegistrationList>(`/v1/me/registrations${params.filter ? `?filter=${params.filter}` : ''}`),\n get: (id: string): Promise<Registration> => this.request<Registration>(`/v1/me/registrations/${encodeURIComponent(id)}`),\n /** Cancel inside the event's cancellation window — `409 window_closed` otherwise. */\n cancel: (id: string): Promise<Registration> => this.request<Registration>(`/v1/me/registrations/${encodeURIComponent(id)}`, { method: 'DELETE' }),\n /** The QR payload of a confirmed registration (what the door scans), or null. */\n tokenPayload: async (id: string): Promise<string | null> => (await this.request<Registration>(`/v1/me/registrations/${encodeURIComponent(id)}`)).token || null\n };\n\n /** The apps the member authorized in this association. */\n readonly connections = {\n list: (): Promise<{ items: Connection[] }> => this.request('/v1/me/connections'),\n revoke: (clientId: string): Promise<void> => this.request<void>(`/v1/me/connections/${encodeURIComponent(clientId)}`, { method: 'DELETE' })\n };\n\n // ---- internals ---------------------------------------------------------------------------------\n\n private async tokenRequest(form: Record<string, string>): Promise<TokenResponse> {\n const res = await withNetworkError(\n this.fetchImpl(`${this.options.apiUrl}/v1/oauth/token`, {\n method: 'POST',\n headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams(form).toString()\n })\n );\n return jsonOrThrow<TokenResponse>(res);\n }\n\n private storeTokens(tokens: TokenResponse): Session {\n const claims = decodeJwtPayload(tokens.access_token);\n const session: Session = {\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token ?? this.getSession()?.refreshToken ?? null,\n expiresAt: Date.now() + Math.max(0, tokens.expires_in) * 1000,\n scope: String(tokens.scope || '').split(/\\s+/).filter(Boolean) as Scope[],\n sub: tokens.sub ?? (typeof claims?.sub === 'string' ? claims.sub : null),\n mode: claims?.mode === 'test' ? 'test' : 'live'\n };\n this.storage.set(this.keys.session, JSON.stringify(session));\n this.emit(session);\n return session;\n }\n\n private clearSession(): void {\n const had = !!this.storage.get(this.keys.session);\n this.storage.remove(this.keys.session);\n if (had) this.emit(null);\n }\n\n private emit(session: Session | null): void {\n for (const l of this.listeners) {\n try {\n l(session);\n } catch {\n /* a listener must not break the others */\n }\n }\n }\n}\n\nconst paramsOf = (url?: string): URLSearchParams | null => {\n if (url) {\n try {\n return new URL(url, 'http://localhost').searchParams;\n } catch {\n return null;\n }\n }\n const loc = (globalThis as unknown as { location?: Location }).location;\n return loc ? new URLSearchParams(loc.search) : null;\n};\n\nconst cleanRedirectParams = (): void => {\n const g = globalThis as unknown as { history?: History; location?: Location };\n if (!g.history || !g.location) return;\n const u = new URL(g.location.href);\n for (const k of ['code', 'state', 'error', 'error_description']) u.searchParams.delete(k);\n try {\n g.history.replaceState(g.history.state, '', u.toString());\n } catch {\n /* ignore */\n }\n};\n\nconst defaultNavigate = (url: string): void => {\n const w = globalThis as unknown as { top?: Window; self?: Window; location?: Location };\n const top = w.top || w.self;\n if (!top) throw new Error('No window to navigate');\n // Navigating the top window from a cross-origin frame throws — which is the refusal we want.\n top.location.assign(url);\n};\n"]}
package/dist/pkce.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export declare const base64url: (bytes: ArrayBuffer | Uint8Array) => string;
2
+ export declare const randomBytes: (n: number) => Uint8Array;
3
+ /** A 43-character verifier (32 random bytes, base64url) — inside RFC 7636's 43–128 range. */
4
+ export declare const createVerifier: () => string;
5
+ /** S256 challenge: base64url(sha256(verifier)). */
6
+ export declare const createChallenge: (verifier: string) => Promise<string>;
7
+ /** An unguessable `state` (16 random bytes) — the CSRF binding between the request and the redirect. */
8
+ export declare const createState: () => string;
9
+ export interface PkcePair {
10
+ verifier: string;
11
+ challenge: string;
12
+ method: 'S256';
13
+ }
14
+ export declare const createPkce: () => Promise<PkcePair>;
package/dist/pkce.js ADDED
@@ -0,0 +1,34 @@
1
+ // PKCE (RFC 7636) and random state, on WebCrypto — available in every modern browser and in Node ≥ 18 as
2
+ // `globalThis.crypto`. No dependencies.
3
+ const subtle = () => {
4
+ const c = globalThis.crypto;
5
+ if (!c || !c.subtle)
6
+ throw new Error('WebCrypto is not available in this environment');
7
+ return c.subtle;
8
+ };
9
+ export const base64url = (bytes) => {
10
+ const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
11
+ let s = '';
12
+ for (const b of arr)
13
+ s += String.fromCharCode(b);
14
+ return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
15
+ };
16
+ export const randomBytes = (n) => {
17
+ const out = new Uint8Array(n);
18
+ globalThis.crypto.getRandomValues(out);
19
+ return out;
20
+ };
21
+ /** A 43-character verifier (32 random bytes, base64url) — inside RFC 7636's 43–128 range. */
22
+ export const createVerifier = () => base64url(randomBytes(32));
23
+ /** S256 challenge: base64url(sha256(verifier)). */
24
+ export const createChallenge = async (verifier) => {
25
+ const digest = await subtle().digest('SHA-256', new TextEncoder().encode(verifier));
26
+ return base64url(digest);
27
+ };
28
+ /** An unguessable `state` (16 random bytes) — the CSRF binding between the request and the redirect. */
29
+ export const createState = () => base64url(randomBytes(16));
30
+ export const createPkce = async () => {
31
+ const verifier = createVerifier();
32
+ return { verifier, challenge: await createChallenge(verifier), method: 'S256' };
33
+ };
34
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AAAA,yGAAyG;AACzG,wCAAwC;AAExC,MAAM,MAAM,GAAG,GAAiB,EAAE;IAC9B,MAAM,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC;IAC5B,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACvF,OAAO,CAAC,CAAC,MAAM,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAA+B,EAAU,EAAE;IACjE,MAAM,GAAG,GAAG,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACxE,IAAI,CAAC,GAAG,EAAE,CAAC;IACX,KAAK,MAAM,CAAC,IAAI,GAAG;QAAE,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjD,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC9E,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAS,EAAc,EAAE;IACjD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IAC9B,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACvC,OAAO,GAAG,CAAC;AACf,CAAC,CAAC;AAEF,6FAA6F;AAC7F,MAAM,CAAC,MAAM,cAAc,GAAG,GAAW,EAAE,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;AAEvE,mDAAmD;AACnD,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,QAAgB,EAAmB,EAAE;IACvE,MAAM,MAAM,GAAG,MAAM,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpF,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC,CAAC;AAEF,wGAAwG;AACxG,MAAM,CAAC,MAAM,WAAW,GAAG,GAAW,EAAE,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;AAQpE,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,IAAuB,EAAE;IACpD,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAC;IAClC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACpF,CAAC,CAAC","sourcesContent":["// PKCE (RFC 7636) and random state, on WebCrypto — available in every modern browser and in Node ≥ 18 as\n// `globalThis.crypto`. No dependencies.\n\nconst subtle = (): SubtleCrypto => {\n const c = globalThis.crypto;\n if (!c || !c.subtle) throw new Error('WebCrypto is not available in this environment');\n return c.subtle;\n};\n\nexport const base64url = (bytes: ArrayBuffer | Uint8Array): string => {\n const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n let s = '';\n for (const b of arr) s += String.fromCharCode(b);\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n};\n\nexport const randomBytes = (n: number): Uint8Array => {\n const out = new Uint8Array(n);\n globalThis.crypto.getRandomValues(out);\n return out;\n};\n\n/** A 43-character verifier (32 random bytes, base64url) — inside RFC 7636's 43–128 range. */\nexport const createVerifier = (): string => base64url(randomBytes(32));\n\n/** S256 challenge: base64url(sha256(verifier)). */\nexport const createChallenge = async (verifier: string): Promise<string> => {\n const digest = await subtle().digest('SHA-256', new TextEncoder().encode(verifier));\n return base64url(digest);\n};\n\n/** An unguessable `state` (16 random bytes) — the CSRF binding between the request and the redirect. */\nexport const createState = (): string => base64url(randomBytes(16));\n\nexport interface PkcePair {\n verifier: string;\n challenge: string;\n method: 'S256';\n}\n\nexport const createPkce = async (): Promise<PkcePair> => {\n const verifier = createVerifier();\n return { verifier, challenge: await createChallenge(verifier), method: 'S256' };\n};\n"]}
@@ -0,0 +1,43 @@
1
+ import type { AssociationPublicConfig, CardKeys, EnrollConfirmResult, EnrollIdentification, Event, EventList, EventListParams, FiscalCodeCheck, FiscalCodeResult, LocaleParam, Registration, Verification } from './types.js';
2
+ export declare const DEFAULT_API_URL = "https://api.vimine.network";
3
+ export interface PublicOptions {
4
+ apiUrl?: string;
5
+ fetch?: typeof fetch;
6
+ }
7
+ export interface PublicApi {
8
+ /** Is this card active right now? Privacy-preserving: status and validity end only. */
9
+ verifyCard(cardToken: string): Promise<Verification>;
10
+ /**
11
+ * An association's public profile by its enrolment slug. `locale`: read its copy — the membership types, the payment
12
+ * instructions, the privacy notice — in that language where the association wrote it; `locale` / `locales` of the
13
+ * answer say what was served and what it offers.
14
+ */
15
+ association(slug: string, params?: LocaleParam): Promise<AssociationPublicConfig>;
16
+ /**
17
+ * Structure + check digit; with the personal data also their consistency with the code — and what the code itself
18
+ * encodes (date and country of birth). For a person born abroad send `country`: their code encodes only the state.
19
+ */
20
+ validateFiscalCode(input: FiscalCodeCheck): Promise<FiscalCodeResult>;
21
+ /** Public keys (JWKS) that verify card tokens offline. Cache and re-fetch daily. */
22
+ cardKeys(): Promise<CardKeys>;
23
+ /**
24
+ * Consume the token of a "confirm your membership request" link (sent when a system enrols an existing account).
25
+ * An account that is not identified yet answers 409 `identification_required` (`errors[]` = what is missing): ask
26
+ * the person and call again with `identification` — it travels in the body, never in the link.
27
+ */
28
+ confirmEnrollment(token: string, identification?: EnrollIdentification): Promise<EnrollConfirmResult>;
29
+ /** An association's PUBLIC events (members-only ones need a member token: `Vimine.events`). */
30
+ events: {
31
+ list(slug: string, params?: EventListParams): Promise<EventList>;
32
+ get(slug: string, event: string, params?: LocaleParam): Promise<Event>;
33
+ };
34
+ /** Consume the token of a guest's "confirm your registration" link: the registration, with the QR `token` when confirmed. */
35
+ confirmRegistration(token: string): Promise<Registration>;
36
+ }
37
+ /** Query string of an events listing (shared by the public, browser and server clients). */
38
+ export declare const eventQuery: (params?: EventListParams) => string;
39
+ /** `?locale=` of a single read. */
40
+ export declare const localeQuery: (params?: LocaleParam) => string;
41
+ export declare const jsonOrThrow: <T>(res: Response) => Promise<T>;
42
+ export declare const withNetworkError: <T>(p: Promise<T>) => Promise<T>;
43
+ export declare const publicApi: (options?: PublicOptions) => PublicApi;