useyuva 0.0.13 → 0.0.14
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/README.md +244 -2
- package/dist/esm/docs.js +1 -1
- package/dist/esm/h3.js +1 -0
- package/dist/esm/index-qcd538se.js +2 -0
- package/dist/esm/nitro/module.js +39 -0
- package/dist/esm/nitro/runtime/core.js +64 -0
- package/dist/esm/nitro/runtime/define.js +3 -0
- package/dist/esm/nitro/runtime/identity.js +3 -0
- package/dist/esm/nitro/runtime/plugin.js +3 -0
- package/dist/esm/nitro/runtime/webhook.js +3 -0
- package/dist/esm/nitro/setup.js +65 -0
- package/dist/esm/nuxt/module.js +64 -2
- package/dist/esm/nuxt/runtime/components.js +8 -1
- package/dist/esm/server.js +1 -0
- package/dist/esm/{thread-6dcd0yms.js → thread-gx5pssds.js} +1 -1
- package/dist/types/h3.d.ts +36 -0
- package/dist/types/nitro/module.d.ts +8 -0
- package/dist/types/nitro/runtime/core.d.ts +41 -0
- package/dist/types/nitro/runtime/define.d.ts +21 -0
- package/dist/types/nitro/setup.d.ts +36 -0
- package/dist/types/nuxt/module.d.ts +17 -3
- package/dist/types/server/analytics.d.ts +52 -0
- package/dist/types/server/encoding.d.ts +6 -0
- package/dist/types/server/identity.d.ts +14 -0
- package/dist/types/server/webhook.d.ts +28 -0
- package/dist/types/server.d.ts +3 -0
- package/package.json +26 -2
package/README.md
CHANGED
|
@@ -10,8 +10,10 @@ JavaScript SDK for [Yuva](https://github.com/productdevbook/yuva), open-source c
|
|
|
10
10
|
| `useyuva/subscribe` | `<yuva-subscribe>`, a mailing list sign-up form. Registers in the browser only. |
|
|
11
11
|
| `useyuva/analytics` | `createAnalytics()`: cookieless pageviews and engagement for an analytics property. Does nothing during SSR. |
|
|
12
12
|
| `useyuva/react` | `YuvaProvider`, `useConversations`, `useMessages`, `YuvaAnalytics`, `useYuvaAnalytics`. React 18+ is an optional peer. |
|
|
13
|
-
| `useyuva/nuxt` | A Nuxt module: `YuvaChat`, `YuvaPageFeedback`, `YuvaDiscussions` and `YuvaSubscribe` components, configured once in `nuxt.config
|
|
13
|
+
| `useyuva/nuxt` | A Nuxt module: `YuvaChat`, `YuvaPageFeedback`, `YuvaDiscussions` and `YuvaSubscribe` components, configured once in `nuxt.config`, and the identity and webhook routes on the server. |
|
|
14
|
+
| `useyuva/nitro` | A Nitro v3 module serving `/_yuva/identity` and `/_yuva/webhook` and sending server-side analytics; `defineYuvaServer` from `useyuva/nitro/runtime`. |
|
|
14
15
|
| `useyuva/api` | A typed `/v1` client for backends (API key), generated from the OpenAPI contract. |
|
|
16
|
+
| `useyuva/server` | `signIdentityToken`, `verifyWebhook` and `createAnalytics` for backends. Web Crypto and `fetch` only: Node 22+, Bun, Deno, workers, srvx. |
|
|
15
17
|
|
|
16
18
|
```sh
|
|
17
19
|
npm install useyuva
|
|
@@ -273,7 +275,46 @@ export default defineNuxtConfig({
|
|
|
273
275
|
`NUXT_PUBLIC_YUVA_SERVER`, `NUXT_PUBLIC_YUVA_CHANNEL`, `NUXT_PUBLIC_YUVA_LOCALE` and, when
|
|
274
276
|
`analytics` is set, `NUXT_PUBLIC_YUVA_ANALYTICS_KEY` override the options at runtime. `analytics`
|
|
275
277
|
takes `key`, `server` (defaults to `yuva.server`), `hash`, `consent`, `manual`, `outbound`, `files` and `chat`, adds a client
|
|
276
|
-
plugin and auto-imports `useYuvaAnalytics()`.
|
|
278
|
+
plugin and auto-imports `useYuvaAnalytics()`.
|
|
279
|
+
|
|
280
|
+
For signed-in users, the only code you write is who they are:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// server/yuva.ts
|
|
284
|
+
export default defineYuvaServer({
|
|
285
|
+
user: async (event) => {
|
|
286
|
+
const { user } = await getUserSession(event); // your session; null when signed out
|
|
287
|
+
return user && { subject: user.id, email: user.email, emailVerified: user.emailConfirmed, name: user.name };
|
|
288
|
+
},
|
|
289
|
+
webhooks: {
|
|
290
|
+
"message.created": async (payload) => {
|
|
291
|
+
if (!payload.data.contact?.online) await sendPush(payload.data);
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
NUXT_YUVA_IDENTITY_SECRET=yuva_is_… # at build and at runtime
|
|
299
|
+
NUXT_YUVA_WEBHOOK_SECRET=whsec_…
|
|
300
|
+
NUXT_YUVA_ANALYTICS_API_KEY=yuva_…
|
|
301
|
+
NUXT_YUVA_ANALYTICS_PROPERTY_ID=…
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
With an identity secret, the module serves `GET /_yuva/identity` (the [h3](#h3) identity handler for
|
|
305
|
+
`user(event)`) and every component fetches its token there on its own; pass `identityToken` to
|
|
306
|
+
override that, or `null` for anonymous. With a webhook secret it serves `POST /_yuva/webhook`, which
|
|
307
|
+
verifies the request and calls `webhooks[type]` like `defineYuvaWebhookHandler`. With an analytics API
|
|
308
|
+
key (`analytics:write`) and property id, `event.context.yuva.track(event)` sends server-side events
|
|
309
|
+
to `yuva.server` (or `NUXT_YUVA_SERVER`), flushed through `waitUntil` after the request where the
|
|
310
|
+
platform has one and when Nitro closes. The secrets are the options `identitySecret`,
|
|
311
|
+
`webhookSecret`, `analytics.apiKey` and `analytics.propertyId` or the variables above; they go only
|
|
312
|
+
into the server runtime config (`useRuntimeConfig().yuva`), never into the public one. A route is
|
|
313
|
+
added only when its secret is known at build time, from `nuxt.config` or the environment; a secret
|
|
314
|
+
that is only in the build environment is not written into the build, so set it at runtime too. A
|
|
315
|
+
route whose secret is empty at runtime answers `404`. `defineYuvaServer` is auto-imported on the
|
|
316
|
+
server (`useyuva/nitro/runtime`), and `event` has your Nuxt's `H3Event` type. Works on Nitro v2 and
|
|
317
|
+
v3. `<YuvaChat />`, `<YuvaPageFeedback />`, `<YuvaDiscussions />` and `<YuvaSubscribe />` are
|
|
277
318
|
auto-imported, render on the server and load their element in the browser when they mount. Props
|
|
278
319
|
are the attributes in camelCase (`pageTitle`, `identityToken`, which also takes a function);
|
|
279
320
|
events are `@rating`, `@feedback`, `@discussion`, `@post`, `@reader` and `@unread`. `yuva-*` tags are custom elements in
|
|
@@ -318,6 +359,207 @@ const { data, error } = await api.GET("/v1/inboxes");
|
|
|
318
359
|
Built on [openapi-fetch](https://openapi-ts.dev/openapi-fetch/); paths, parameters and bodies are
|
|
319
360
|
typed from the contract. Keep API keys on the server.
|
|
320
361
|
|
|
362
|
+
## Server
|
|
363
|
+
|
|
364
|
+
For your backend: sign identity tokens, verify webhooks and send server-side analytics events. It
|
|
365
|
+
uses only Web Crypto, `Request` and `fetch`, so it runs on Node, Bun, Deno and workers, and directly
|
|
366
|
+
under [srvx](https://srvx.h3.dev):
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { serve } from "srvx";
|
|
370
|
+
import { createAnalytics, signIdentityToken, verifyWebhook, WebhookVerificationError } from "useyuva/server";
|
|
371
|
+
|
|
372
|
+
const analytics = createAnalytics({
|
|
373
|
+
server: "https://support.example.com",
|
|
374
|
+
apiKey: process.env.YUVA_API_KEY!, // analytics:write
|
|
375
|
+
propertyId: process.env.YUVA_ANALYTICS_PROPERTY!,
|
|
376
|
+
onError: (error) => console.warn("yuva analytics", error),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
serve({
|
|
380
|
+
async fetch(request) {
|
|
381
|
+
const { pathname } = new URL(request.url);
|
|
382
|
+
if (pathname === "/yuva-token") {
|
|
383
|
+
const user = await currentUser(request); // your session
|
|
384
|
+
const token = await signIdentityToken(process.env.YUVA_IDENTITY_SECRET!, {
|
|
385
|
+
subject: user.id, // required: your user id
|
|
386
|
+
email: user.email,
|
|
387
|
+
emailVerified: user.emailConfirmed, // only then does Yuva match the address to a contact
|
|
388
|
+
name: user.name,
|
|
389
|
+
attrs: { plan: user.plan },
|
|
390
|
+
});
|
|
391
|
+
return Response.json({ token });
|
|
392
|
+
}
|
|
393
|
+
if (pathname === "/webhooks/yuva" && request.method === "POST") {
|
|
394
|
+
try {
|
|
395
|
+
const event = await verifyWebhook(process.env.YUVA_WEBHOOK_SECRET!, request);
|
|
396
|
+
// Ignore a webhook-id you have already handled; Yuva retries until it gets a 2xx.
|
|
397
|
+
if (event.type === "message.created" && !event.data.contact?.online) {
|
|
398
|
+
// send your push notification
|
|
399
|
+
}
|
|
400
|
+
return new Response(null, { status: 204 });
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error instanceof WebhookVerificationError) return new Response("bad signature", { status: 401 });
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (pathname === "/signup" && request.method === "POST") {
|
|
407
|
+
// ...
|
|
408
|
+
analytics.track(request, { name: "Signup", props: { plan: "pro" }, revenue: { amount: "29.90", currency: "EUR" } });
|
|
409
|
+
request.waitUntil?.(analytics.flush());
|
|
410
|
+
return Response.redirect(new URL("/welcome", request.url), 303);
|
|
411
|
+
}
|
|
412
|
+
return new Response("Not found", { status: 404 });
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
`signIdentityToken(secret, claims)` returns the HS256 token the widget and the mobile SDKs exchange
|
|
418
|
+
for a contact session, signed with the inbox's identity secret (shown once when the inbox is created
|
|
419
|
+
or its secret is rotated). Claims: `subject` (1 to 200 characters), `email`, `emailVerified`, `name`,
|
|
420
|
+
`locale`, `attrs` (at most 16 KiB as JSON), `id` (the `jti`: Yuva then accepts the token only once),
|
|
421
|
+
`issuedAt` and `ttl` in seconds (300 by default, at most 600). Sign a new one whenever the page or
|
|
422
|
+
app starts a session.
|
|
423
|
+
|
|
424
|
+
`verifyWebhook(secret, request, { tolerance })` checks the [Standard Webhooks](https://www.standardwebhooks.com)
|
|
425
|
+
signature over the raw body with the endpoint's `whsec_…` secret and returns the parsed event, typed
|
|
426
|
+
by its `type`. It throws a `WebhookVerificationError` (`code`: `invalid_secret`, `missing_headers`,
|
|
427
|
+
`invalid_timestamp`, `timestamp_too_old`, `timestamp_too_new`, `invalid_signature`) when no
|
|
428
|
+
signature matches or `webhook-timestamp` is more than `tolerance` seconds (300 by default) from now.
|
|
429
|
+
During a secret rotation either secret verifies. When the body is already read,
|
|
430
|
+
`verifyWebhookBody(secret, headers, bytes, options)` does the same; `signWebhook(secret, id, time, body)`
|
|
431
|
+
signs a request for testing a handler.
|
|
432
|
+
|
|
433
|
+
`createAnalytics({ server, apiKey, propertyId })` reports events only your backend sees, such as a
|
|
434
|
+
signup or a payment, to an analytics property with an API key that has the `analytics:write` scope.
|
|
435
|
+
`track(request, event)` forwards the visitor's address, `User-Agent`, `Sec-CH-UA-Platform`,
|
|
436
|
+
`Sec-CH-UA-Mobile` and `Accept-Language` (never stored by Yuva), so the event counts as the same
|
|
437
|
+
visitor as their browser that day, and takes the URL and referrer from the request when the event
|
|
438
|
+
leaves them out; requests with `DNT: 1` or `Sec-GPC: 1` send nothing. `send(event)` queues an event
|
|
439
|
+
as it is, such as one with your own `visitorKey` (a session id, never an e-mail address) instead of
|
|
440
|
+
`ip` and `userAgent`. Both throw for an event without a name or URL and after `close()`.
|
|
441
|
+
|
|
442
|
+
The visitor's address is read only from `request.ip`, which srvx sets from the connection. Behind a
|
|
443
|
+
proxy, or on a runtime that does not set it, pass `clientIP: (request) => …` returning the address
|
|
444
|
+
your proxy reports, such as `request.headers.get("cf-connecting-ip")` on Cloudflare Workers; a
|
|
445
|
+
forwarded header is trustworthy only when your own proxy sets it.
|
|
446
|
+
|
|
447
|
+
Events are sent in batches of up to 100 every `flushInterval` (5000 ms) or as soon as a batch is
|
|
448
|
+
full, holding at most `queueSize` (10,000) events. Network errors, `429` and `5xx` are retried
|
|
449
|
+
`maxRetries` times (5) with a backoff from `minBackoff` (1000 ms) doubling up to a minute, never
|
|
450
|
+
sooner than `Retry-After`. Dropped events, refused batches (`AnalyticsBatchError`) and events the
|
|
451
|
+
server left out (`AnalyticsEventError`) go to `onError`. The timers never keep a process alive:
|
|
452
|
+
`flush()` sends what is queued and resolves to the last batch error, if any, without rejecting, so
|
|
453
|
+
it can go to `waitUntil`; `close()` flushes and stops the client before the process exits.
|
|
454
|
+
|
|
455
|
+
### h3
|
|
456
|
+
|
|
457
|
+
`useyuva/h3` wraps the same helpers for an [h3](https://h3.dev) v2 app (`h3` is an optional peer
|
|
458
|
+
dependency):
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
import { H3, serve } from "h3";
|
|
462
|
+
import { defineYuvaAnalytics, defineYuvaIdentityHandler, defineYuvaWebhookHandler } from "useyuva/h3";
|
|
463
|
+
import { createAnalytics } from "useyuva/server";
|
|
464
|
+
|
|
465
|
+
const app = new H3()
|
|
466
|
+
.use(
|
|
467
|
+
defineYuvaAnalytics({
|
|
468
|
+
analytics: createAnalytics({
|
|
469
|
+
server: "https://support.example.com",
|
|
470
|
+
apiKey: process.env.YUVA_API_KEY!,
|
|
471
|
+
propertyId: process.env.YUVA_ANALYTICS_PROPERTY!,
|
|
472
|
+
}),
|
|
473
|
+
})
|
|
474
|
+
)
|
|
475
|
+
.get(
|
|
476
|
+
"/yuva-token",
|
|
477
|
+
defineYuvaIdentityHandler({
|
|
478
|
+
secret: process.env.YUVA_IDENTITY_SECRET!,
|
|
479
|
+
user: async (event) => {
|
|
480
|
+
const user = await currentUser(event); // your session; null when signed out
|
|
481
|
+
return user && { subject: user.id, email: user.email, emailVerified: user.emailConfirmed, name: user.name };
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
)
|
|
485
|
+
.post(
|
|
486
|
+
"/webhooks/yuva",
|
|
487
|
+
defineYuvaWebhookHandler({
|
|
488
|
+
secret: process.env.YUVA_WEBHOOK_SECRET!,
|
|
489
|
+
on: {
|
|
490
|
+
"message.created": async (payload) => {
|
|
491
|
+
if (!payload.data.contact?.online) await sendPush(payload.data);
|
|
492
|
+
},
|
|
493
|
+
"conversation.rated": (payload) => saveRating(payload.data.conversation),
|
|
494
|
+
},
|
|
495
|
+
})
|
|
496
|
+
)
|
|
497
|
+
.post("/signup", async (event) => {
|
|
498
|
+
// ...
|
|
499
|
+
event.context.yuva?.track({ name: "Signup", props: { plan: "pro" } });
|
|
500
|
+
return Response.redirect(new URL("/welcome", event.req.url), 303);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
serve(app);
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
```html
|
|
507
|
+
<yuva-chat channel="yuva_pk_…" server="https://support.example.com"></yuva-chat>
|
|
508
|
+
<script type="module">
|
|
509
|
+
import "useyuva/chat";
|
|
510
|
+
document.querySelector("yuva-chat").setIdentityToken(async () => {
|
|
511
|
+
const response = await fetch("/yuva-token", { credentials: "same-origin" });
|
|
512
|
+
return response.ok ? (await response.json()).token : null;
|
|
513
|
+
});
|
|
514
|
+
</script>
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
`defineYuvaIdentityHandler({ secret, user, ttl })` answers `{ "token": "…" }` with a token for the
|
|
518
|
+
claims `user(event)` returns, with `Cache-Control: no-store`, and `401` when it returns nothing;
|
|
519
|
+
`ttl` is the default for claims without their own. `defineYuvaWebhookHandler({ secret, tolerance, on })`
|
|
520
|
+
verifies the raw request with `verifyWebhook` and calls the handler for the event's `type` with the
|
|
521
|
+
event typed for it; it answers `401` when the signature does not verify, `204` for a type without a
|
|
522
|
+
handler, and otherwise the `Response` the handler returns or `204`. A handler that throws answers
|
|
523
|
+
`500`, so Yuva retries. `defineYuvaAnalytics({ analytics })` is a middleware that puts
|
|
524
|
+
`track(event)` (`analytics.track` with the request) on `event.context.yuva` and, after a request
|
|
525
|
+
that tracked something, hands `flush()` to the request's `waitUntil`, which srvx sets on every
|
|
526
|
+
runtime it serves; without one the queue goes out on the client's timer.
|
|
527
|
+
|
|
528
|
+
### Nitro
|
|
529
|
+
|
|
530
|
+
`useyuva/nitro` is the same server part for a [Nitro](https://nitro.build) v3 app:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
// nitro.config.ts
|
|
534
|
+
import { defineConfig } from "nitro";
|
|
535
|
+
|
|
536
|
+
export default defineConfig({
|
|
537
|
+
serverDir: "./server",
|
|
538
|
+
modules: ["useyuva/nitro"],
|
|
539
|
+
});
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
// server/yuva.ts
|
|
544
|
+
import { getCookie } from "nitro/h3";
|
|
545
|
+
import { defineYuvaServer } from "useyuva/nitro/runtime";
|
|
546
|
+
|
|
547
|
+
export default defineYuvaServer({
|
|
548
|
+
user: async (event) => {
|
|
549
|
+
const user = await findSession(getCookie(event, "session")); // null when signed out
|
|
550
|
+
return user && { subject: user.id, email: user.email, emailVerified: user.emailConfirmed, name: user.name };
|
|
551
|
+
},
|
|
552
|
+
webhooks: { "conversation.rated": (payload) => saveRating(payload.data.conversation) },
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
The variables are `NITRO_YUVA_IDENTITY_SECRET`, `NITRO_YUVA_WEBHOOK_SECRET`,
|
|
557
|
+
`NITRO_YUVA_ANALYTICS_API_KEY`, `NITRO_YUVA_ANALYTICS_PROPERTY_ID` and `NITRO_YUVA_SERVER` (the Yuva
|
|
558
|
+
server analytics goes to), or `yuva({ identitySecret, webhookSecret, analytics: { apiKey, propertyId }, server })`
|
|
559
|
+
from `useyuva/nitro` in `modules`; routes, `track` and the build-time rule are as in [Nuxt](#nuxt).
|
|
560
|
+
`yuva.ts` is read from `serverDir`, or the root without one. The page asks for the token as in the
|
|
561
|
+
h3 example, at `/_yuva/identity`.
|
|
562
|
+
|
|
321
563
|
## License
|
|
322
564
|
|
|
323
565
|
MIT
|
package/dist/esm/docs.js
CHANGED
|
@@ -131,4 +131,4 @@ button { font: inherit; color: inherit; }
|
|
|
131
131
|
.choice[aria-pressed="true"] { background: var(--yuva-soft); }
|
|
132
132
|
.choice:disabled { cursor: default; }
|
|
133
133
|
.choice:focus-visible { outline: 2px solid var(--yuva-accent); outline-offset: 2px; }
|
|
134
|
-
`;function storageKey(channel){return`yuva:${channel}:page-ratings`}function remembered(channel){try{let raw=localStorage.getItem(storageKey(channel));return raw?JSON.parse(raw):{}}catch{return{}}}function remember(channel,page,rating){try{let ratings=remembered(channel);delete ratings[page],ratings[page]=rating;let pages=Object.keys(ratings);for(let old of pages.slice(0,Math.max(0,pages.length-maxRemembered)))delete ratings[old];localStorage.setItem(storageKey(channel),JSON.stringify(ratings))}catch{}}class YuvaPageFeedbackElement extends YuvaDocsElement{#phase="ask";#rating;#error="";#comment=el("textarea");#email=el("input");constructor(){super();this.#comment.rows=3,this.#comment.maxLength=5000,this.#comment.dir="auto",this.#comment.addEventListener("input",()=>this.#syncSend()),this.#email.type="email",this.#email.autocomplete="email"}get rating(){return this.#rating??null}reset(){this.#phase="ask",this.#error="",this.#comment.value="",this.#rating=this.channel?remembered(this.channel)[this.pageUrl]:void 0}pageChanged(){if(this.#phase==="ask"&&!this.#error)this.#rating=this.channel?remembered(this.channel)[this.pageUrl]:void 0;this.render()}async rate(rating){let channel=this.channel;if(!channel||this.refused||this.#phase==="rating"||this.#phase==="sending")return;let page=this.pageUrl,previous=remembered(channel)[page];if(this.#error="",previous===rating){this.#rating=rating,this.#phase=this.canWrite?"form":"thanks",this.render(),this.#comment.focus({preventScroll:!0});return}this.#phase="rating",this.#rating=rating,this.render();try{await this.client().ratePage({page,title:this.pageTitle||void 0,rating,previous})}catch(error){if(this.pageUrl!==page||this.refusal(error))return;this.#rating=previous,this.#phase="ask",this.#error=this.errorText(error),this.render();return}if(remember(channel,page,rating),this.emit("yuva-rating",{page,rating,previous:previous??null}),this.pageUrl!==page)return;this.#phase=this.canWrite?"form":"thanks",this.render(),this.#comment.focus({preventScroll:!0})}async#send(){let body=this.#comment.value.trim(),email=this.#email.value.trim(),rating=this.#rating;if(!body||this.#phase!=="form")return;if(email&&!this.#email.checkValidity()){this.#error=this.t({id:"E_qvzP"}),this.render();return}let page=this.pageUrl;this.#phase="sending",this.#error="",this.render();try{let{conversation}=await this.client().sendFeedback({page_url:page,page_title:this.pageTitle||void 0,rating,body,locale:this.locale,allow_email:this.identified||!!email,email:!this.identified&&email?email:void 0});if(this.emit("yuva-feedback",{page,rating:rating??null,conversation_id:conversation.id}),this.pageUrl!==page)return;this.#comment.value="",this.#phase="thanks"}catch(error){if(this.pageUrl!==page||this.refusal(error,!1))return;this.#phase="form",this.#error=this.errorText(error)}this.render()}#syncSend(){let send=this.root.querySelector(".send");if(send)send.disabled=this.#phase!=="form"||this.#comment.value.trim()===""}render(){if(this.refused||!this.channel){this.root.replaceChildren();return}let question=this.t({id:"SOvBw9"}),ask=el("div","ask");ask.setAttribute("role","group"),ask.setAttribute("aria-label",question);let choices=el("div","choices");for(let value of["up","down"]){let button=el("button","choice");button.type="button",button.innerHTML=thumb(value==="down"),button.append(el("span","",value==="up"?this.t({id:"l75CjT"}):this.t({id:"1UzENP"}))),button.setAttribute("aria-pressed",String(this.#rating===value)),button.disabled=this.#phase==="rating"||this.#phase==="sending",button.addEventListener("click",()=>void this.rate(value)),choices.append(button)}ask.append(el("p","question",question),choices);let children=[ask];if(this.#phase==="form"||this.#phase==="sending"){let form=el("form");form.noValidate=!0,form.addEventListener("submit",(event)=>{event.preventDefault(),this.#send()});let prompt=this.#rating==="down"?this.t({id:"U7H5Fm"}):this.t({id:"EmiQvf"});if(this.#comment.placeholder=prompt,this.#comment.setAttribute("aria-label",prompt),this.#comment.disabled=this.#phase==="sending",form.append(this.#comment),!this.identified)this.#email.placeholder=this.t({id:"a_qtPP"}),this.#email.setAttribute("aria-label",this.t({id:"XJURKP"})),this.#email.disabled=this.#phase==="sending",form.append(this.#email);let skip=el("button","link",this.t({id:"tA4dAO"}));skip.type="button",skip.disabled=this.#phase==="sending",skip.addEventListener("click",()=>{this.#phase="thanks",this.#error="",this.render()});let send=el("button","send",this.#phase==="sending"?this.t({id:"Sad2tK"}):this.t({id:"JlFcis"}));send.type="submit";let actions=el("div","actions");actions.append(skip,send),form.append(actions),children.push(form)}else if(this.#phase==="thanks"){let thanks=el("p","muted",this.t({id:"d6N994"}));thanks.setAttribute("role","status"),children.push(thanks)}if(this.#error){let error=el("p","error",this.#error);error.setAttribute("role","alert"),children.push(error)}this.frame(styles2,...children),this.#syncSend()}}function defineDocs(){if(typeof customElements>"u")return;if(!customElements.get("yuva-discussions"))customElements.define("yuva-discussions",YuvaDiscussionsElement);if(!customElements.get("yuva-page-feedback"))customElements.define("yuva-page-feedback",YuvaPageFeedbackElement)}YuvaDiscussionsElement.loadThread=()=>import("./thread-
|
|
134
|
+
`;function storageKey(channel){return`yuva:${channel}:page-ratings`}function remembered(channel){try{let raw=localStorage.getItem(storageKey(channel));return raw?JSON.parse(raw):{}}catch{return{}}}function remember(channel,page,rating){try{let ratings=remembered(channel);delete ratings[page],ratings[page]=rating;let pages=Object.keys(ratings);for(let old of pages.slice(0,Math.max(0,pages.length-maxRemembered)))delete ratings[old];localStorage.setItem(storageKey(channel),JSON.stringify(ratings))}catch{}}class YuvaPageFeedbackElement extends YuvaDocsElement{#phase="ask";#rating;#error="";#comment=el("textarea");#email=el("input");constructor(){super();this.#comment.rows=3,this.#comment.maxLength=5000,this.#comment.dir="auto",this.#comment.addEventListener("input",()=>this.#syncSend()),this.#email.type="email",this.#email.autocomplete="email"}get rating(){return this.#rating??null}reset(){this.#phase="ask",this.#error="",this.#comment.value="",this.#rating=this.channel?remembered(this.channel)[this.pageUrl]:void 0}pageChanged(){if(this.#phase==="ask"&&!this.#error)this.#rating=this.channel?remembered(this.channel)[this.pageUrl]:void 0;this.render()}async rate(rating){let channel=this.channel;if(!channel||this.refused||this.#phase==="rating"||this.#phase==="sending")return;let page=this.pageUrl,previous=remembered(channel)[page];if(this.#error="",previous===rating){this.#rating=rating,this.#phase=this.canWrite?"form":"thanks",this.render(),this.#comment.focus({preventScroll:!0});return}this.#phase="rating",this.#rating=rating,this.render();try{await this.client().ratePage({page,title:this.pageTitle||void 0,rating,previous})}catch(error){if(this.pageUrl!==page||this.refusal(error))return;this.#rating=previous,this.#phase="ask",this.#error=this.errorText(error),this.render();return}if(remember(channel,page,rating),this.emit("yuva-rating",{page,rating,previous:previous??null}),this.pageUrl!==page)return;this.#phase=this.canWrite?"form":"thanks",this.render(),this.#comment.focus({preventScroll:!0})}async#send(){let body=this.#comment.value.trim(),email=this.#email.value.trim(),rating=this.#rating;if(!body||this.#phase!=="form")return;if(email&&!this.#email.checkValidity()){this.#error=this.t({id:"E_qvzP"}),this.render();return}let page=this.pageUrl;this.#phase="sending",this.#error="",this.render();try{let{conversation}=await this.client().sendFeedback({page_url:page,page_title:this.pageTitle||void 0,rating,body,locale:this.locale,allow_email:this.identified||!!email,email:!this.identified&&email?email:void 0});if(this.emit("yuva-feedback",{page,rating:rating??null,conversation_id:conversation.id}),this.pageUrl!==page)return;this.#comment.value="",this.#phase="thanks"}catch(error){if(this.pageUrl!==page||this.refusal(error,!1))return;this.#phase="form",this.#error=this.errorText(error)}this.render()}#syncSend(){let send=this.root.querySelector(".send");if(send)send.disabled=this.#phase!=="form"||this.#comment.value.trim()===""}render(){if(this.refused||!this.channel){this.root.replaceChildren();return}let question=this.t({id:"SOvBw9"}),ask=el("div","ask");ask.setAttribute("role","group"),ask.setAttribute("aria-label",question);let choices=el("div","choices");for(let value of["up","down"]){let button=el("button","choice");button.type="button",button.innerHTML=thumb(value==="down"),button.append(el("span","",value==="up"?this.t({id:"l75CjT"}):this.t({id:"1UzENP"}))),button.setAttribute("aria-pressed",String(this.#rating===value)),button.disabled=this.#phase==="rating"||this.#phase==="sending",button.addEventListener("click",()=>void this.rate(value)),choices.append(button)}ask.append(el("p","question",question),choices);let children=[ask];if(this.#phase==="form"||this.#phase==="sending"){let form=el("form");form.noValidate=!0,form.addEventListener("submit",(event)=>{event.preventDefault(),this.#send()});let prompt=this.#rating==="down"?this.t({id:"U7H5Fm"}):this.t({id:"EmiQvf"});if(this.#comment.placeholder=prompt,this.#comment.setAttribute("aria-label",prompt),this.#comment.disabled=this.#phase==="sending",form.append(this.#comment),!this.identified)this.#email.placeholder=this.t({id:"a_qtPP"}),this.#email.setAttribute("aria-label",this.t({id:"XJURKP"})),this.#email.disabled=this.#phase==="sending",form.append(this.#email);let skip=el("button","link",this.t({id:"tA4dAO"}));skip.type="button",skip.disabled=this.#phase==="sending",skip.addEventListener("click",()=>{this.#phase="thanks",this.#error="",this.render()});let send=el("button","send",this.#phase==="sending"?this.t({id:"Sad2tK"}):this.t({id:"JlFcis"}));send.type="submit";let actions=el("div","actions");actions.append(skip,send),form.append(actions),children.push(form)}else if(this.#phase==="thanks"){let thanks=el("p","muted",this.t({id:"d6N994"}));thanks.setAttribute("role","status"),children.push(thanks)}if(this.#error){let error=el("p","error",this.#error);error.setAttribute("role","alert"),children.push(error)}this.frame(styles2,...children),this.#syncSend()}}function defineDocs(){if(typeof customElements>"u")return;if(!customElements.get("yuva-discussions"))customElements.define("yuva-discussions",YuvaDiscussionsElement);if(!customElements.get("yuva-page-feedback"))customElements.define("yuva-page-feedback",YuvaPageFeedbackElement)}YuvaDiscussionsElement.loadThread=()=>import("./thread-gx5pssds.js");defineDocs();export{YuvaDiscussionsElement,YuvaPageFeedbackElement,defineDocs};
|
package/dist/esm/h3.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{signIdentityToken2,WebhookVerificationError2,verifyWebhook2}from"./index-qcd538se.js";import{defineHandler,defineMiddleware}from"h3";var noStore={"cache-control":"no-store"};function defineYuvaIdentityHandler({secret,user,ttl}){return defineHandler(async(event)=>{let claims=await user(event);if(!claims)return new Response(null,{status:401,headers:noStore});let token=await signIdentityToken2(secret,{...claims,ttl:claims.ttl??ttl});return Response.json({token},{headers:noStore})})}function defineYuvaWebhookHandler({secret,tolerance,on}){return defineHandler(async(event)=>{let payload;try{payload=await verifyWebhook2(secret,event.req,{tolerance})}catch(error){if(error instanceof WebhookVerificationError2)return new Response(error.message,{status:401});throw error}if(!Object.hasOwn(on,payload.type))return new Response(null,{status:204});let handler=on[payload.type],result=await handler?.(payload,event);return result instanceof Response?result:new Response(null,{status:204})})}function defineYuvaAnalytics({analytics}){return defineMiddleware(async(event,next)=>{let request=event.req,tracked=!1;event.context.yuva={track(e){analytics.track(request,e),tracked=!0}};try{return await next()}finally{if(tracked&&request.waitUntil)event.waitUntil(analytics.flush())}})}export{defineYuvaAnalytics,defineYuvaIdentityHandler,defineYuvaWebhookHandler};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var encoder=new TextEncoder,utf8=(text)=>encoder.encode(text);function base64(bytes){let binary="";for(let byte of bytes)binary+=String.fromCharCode(byte);return btoa(binary)}var base64url=(bytes)=>base64(bytes).replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_"),strictBase64=/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;function decodeBase64(text){if(!strictBase64.test(text))return;return Uint8Array.from(atob(text),(char)=>char.charCodeAt(0))}var hmacKey=(key,usages)=>crypto.subtle.importKey("raw",key,{name:"HMAC",hash:"SHA-256"},!1,usages),goEscapes={"<":"\\u003c",">":"\\u003e","&":"\\u0026","\u2028":"\\u2028","\u2029":"\\u2029"};function goJSON(value){if(value===null||typeof value!=="object"||typeof value.toJSON==="function")return JSON.stringify(value)?.replace(/[<>&\u2028\u2029]/g,(char)=>goEscapes[char]);if(Array.isArray(value))return`[${value.map((item)=>goJSON(item)??"null").join(",")}]`;let fields=[];for(let key of Object.keys(value).sort()){let encoded=goJSON(value[key]);if(encoded!==void 0)fields.push(`${goJSON(key)}:${encoded}`)}return`{${fields.join(",")}}`}var DEFAULT_IDENTITY_TTL2=300,MAX_IDENTITY_TTL2=600,header=base64url(utf8('{"alg":"HS256","typ":"JWT"}'));async function signIdentityToken2(secret,claims){if(!secret)throw Error("identity: the inbox identity secret is empty");let sub=(claims.subject??"").trim();if(!sub||[...sub].length>200)throw Error("identity: subject must be 1 to 200 characters");let ttl=claims.ttl||DEFAULT_IDENTITY_TTL2;if(!(ttl>0&&ttl<=MAX_IDENTITY_TTL2))throw Error("identity: ttl must be at most 10 minutes");let at=(claims.issuedAt??new Date).getTime(),payload={sub,email:claims.email||void 0,email_verified:claims.emailVerified||void 0,name:claims.name||void 0,locale:claims.locale||void 0,attrs:claims.attrs&&Object.keys(claims.attrs).length>0?claims.attrs:void 0,jti:claims.id||void 0,iat:Math.floor(at/1000),exp:Math.floor((at+ttl*1000)/1000)},fields=Object.entries(payload).flatMap(([key,value])=>{let encoded=goJSON(value);return encoded===void 0?[]:[`"${key}":${encoded}`]}),signing=`${header}.${base64url(utf8(`{${fields.join(",")}}`))}`,key=await hmacKey(utf8(secret),["sign"]);return`${signing}.${base64url(new Uint8Array(await crypto.subtle.sign("HMAC",key,utf8(signing))))}`}var DEFAULT_WEBHOOK_TOLERANCE2=300,messages={invalid_secret:"webhook: the secret is not a whsec_ base64 secret",missing_headers:"webhook: webhook-id, webhook-timestamp or webhook-signature is missing",invalid_timestamp:"webhook: webhook-timestamp is not a unix time",timestamp_too_old:"webhook: webhook-timestamp is too old",timestamp_too_new:"webhook: webhook-timestamp is too far in the future",invalid_signature:"webhook: no signature matches"};class WebhookVerificationError2 extends Error{code;constructor(code){super(messages[code]);this.name="WebhookVerificationError",this.code=code}}function key(secret){let bytes=decodeBase64(secret.trim().replace(/^whsec_/,""));if(!bytes||bytes.length===0)throw new WebhookVerificationError2("invalid_secret");return bytes}function signed(id,timestamp,body){let prefix=utf8(`${id}.${timestamp}.`),data=new Uint8Array(prefix.length+body.length);return data.set(prefix),data.set(body,prefix.length),data}async function signWebhook2(secret,id,timestamp,body){let k=await hmacKey(key(secret),["sign"]),data=signed(id,String(Math.floor(timestamp.getTime()/1000)),typeof body==="string"?utf8(body):body);return`v1,${base64(new Uint8Array(await crypto.subtle.sign("HMAC",k,data)))}`}async function verifyWebhookBody2(secret,headers,body,{tolerance,now=new Date}={}){let raw=key(secret),id=headers.get("webhook-id"),ts=headers.get("webhook-timestamp"),sigs=headers.get("webhook-signature");if(!id||!ts||!sigs)throw new WebhookVerificationError2("missing_headers");if(!/^[+-]?\d{1,19}$/.test(ts))throw new WebhookVerificationError2("invalid_timestamp");let window=(tolerance&&tolerance>0?tolerance:DEFAULT_WEBHOOK_TOLERANCE2)*1000,at=Number(ts)*1000;if(now.getTime()-at>window)throw new WebhookVerificationError2("timestamp_too_old");if(at-now.getTime()>window)throw new WebhookVerificationError2("timestamp_too_new");let k=await hmacKey(raw,["verify"]),data=signed(id,ts,body);for(let sig of sigs.split(/\s+/)){let comma=sig.indexOf(",");if(comma<0||sig.slice(0,comma)!=="v1")continue;let got=decodeBase64(sig.slice(comma+1));if(got&&got.length>0&&await crypto.subtle.verify("HMAC",k,got,data))return}throw new WebhookVerificationError2("invalid_signature")}async function verifyWebhook2(secret,request,options){let body=new Uint8Array(await request.arrayBuffer());return await verifyWebhookBody2(secret,request.headers,body,options),JSON.parse(new TextDecoder().decode(body))}
|
|
2
|
+
export{DEFAULT_IDENTITY_TTL2,MAX_IDENTITY_TTL2,signIdentityToken2,DEFAULT_WEBHOOK_TOLERANCE2,WebhookVerificationError2,signWebhook2,verifyWebhookBody2,verifyWebhook2};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
findServerFile,
|
|
4
|
+
IDENTITY_ROUTE,
|
|
5
|
+
runtimePath,
|
|
6
|
+
SERVER_MODULE,
|
|
7
|
+
serverModule,
|
|
8
|
+
serverParts,
|
|
9
|
+
serverRuntimeConfig,
|
|
10
|
+
WEBHOOK_ROUTE
|
|
11
|
+
} from "./setup.js";
|
|
12
|
+
export function yuva(options = {}) {
|
|
13
|
+
return {
|
|
14
|
+
name: "useyuva",
|
|
15
|
+
setup(nitro) {
|
|
16
|
+
const config = serverRuntimeConfig(nitro.options.runtimeConfig.yuva, options);
|
|
17
|
+
nitro.options.runtimeConfig.yuva = config;
|
|
18
|
+
const parts = serverParts(config, process.env, "NITRO_");
|
|
19
|
+
if (!parts.identity && !parts.webhook && !parts.analytics)
|
|
20
|
+
return;
|
|
21
|
+
const dir = nitro.options.serverDir ? resolve(nitro.options.rootDir, nitro.options.serverDir) : nitro.options.rootDir;
|
|
22
|
+
const server = findServerFile(dir);
|
|
23
|
+
if (!server && parts.identity)
|
|
24
|
+
nitro.logger.warn(`useyuva: no yuva.ts in ${dir}, so ${IDENTITY_ROUTE} answers 404`);
|
|
25
|
+
nitro.options.virtual[SERVER_MODULE] = serverModule({
|
|
26
|
+
nitro: 3,
|
|
27
|
+
server,
|
|
28
|
+
runtimeConfig: "nitro/runtime-config"
|
|
29
|
+
});
|
|
30
|
+
if (parts.identity)
|
|
31
|
+
nitro.options.handlers.push({ route: IDENTITY_ROUTE, method: "GET", handler: runtimePath("identity") });
|
|
32
|
+
if (parts.webhook)
|
|
33
|
+
nitro.options.handlers.push({ route: WEBHOOK_ROUTE, method: "POST", handler: runtimePath("webhook") });
|
|
34
|
+
if (parts.analytics)
|
|
35
|
+
nitro.options.plugins.push(runtimePath("plugin"));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export default yuva();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAnalytics,
|
|
3
|
+
signIdentityToken,
|
|
4
|
+
verifyWebhook,
|
|
5
|
+
WebhookVerificationError
|
|
6
|
+
} from "../../server.js";
|
|
7
|
+
const noStore = { "cache-control": "no-store" };
|
|
8
|
+
const notFound = () => new Response(null, { status: 404 });
|
|
9
|
+
export function identityHandler(yuva) {
|
|
10
|
+
return yuva.defineHandler(async (event) => {
|
|
11
|
+
const secret = yuva.config().yuva?.identitySecret;
|
|
12
|
+
if (!secret || !yuva.server.user)
|
|
13
|
+
return notFound();
|
|
14
|
+
const claims = await yuva.server.user(event);
|
|
15
|
+
if (!claims)
|
|
16
|
+
return new Response(null, { status: 401, headers: noStore });
|
|
17
|
+
return Response.json({ token: await signIdentityToken(secret, claims) }, { headers: noStore });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function webhookHandler(yuva) {
|
|
21
|
+
return yuva.defineHandler(async (event) => {
|
|
22
|
+
const secret = yuva.config().yuva?.webhookSecret;
|
|
23
|
+
if (!secret)
|
|
24
|
+
return notFound();
|
|
25
|
+
let payload;
|
|
26
|
+
try {
|
|
27
|
+
payload = await verifyWebhook(secret, yuva.toRequest(event));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof WebhookVerificationError)
|
|
30
|
+
return new Response(error.message, { status: 401 });
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
const on = yuva.server.webhooks ?? {};
|
|
34
|
+
if (!Object.hasOwn(on, payload.type))
|
|
35
|
+
return new Response(null, { status: 204 });
|
|
36
|
+
const handler = on[payload.type];
|
|
37
|
+
const result = await handler?.(payload, event);
|
|
38
|
+
return result instanceof Response ? result : new Response(null, { status: 204 });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function analyticsPlugin(yuva) {
|
|
42
|
+
return (nitroApp) => {
|
|
43
|
+
const config = yuva.config();
|
|
44
|
+
const { apiKey, propertyId } = config.yuva?.analytics ?? {};
|
|
45
|
+
const server = config.yuva?.server || config.public?.yuva?.analytics?.server || config.public?.yuva?.server;
|
|
46
|
+
if (!apiKey || !propertyId || !server)
|
|
47
|
+
return;
|
|
48
|
+
const analytics = createAnalytics({ server, apiKey, propertyId });
|
|
49
|
+
const tracked = new WeakSet;
|
|
50
|
+
nitroApp.hooks.hook("request", (event) => {
|
|
51
|
+
event.context.yuva = {
|
|
52
|
+
track(e) {
|
|
53
|
+
analytics.track(yuva.trackRequest(event), e);
|
|
54
|
+
tracked.add(event);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
yuva.onResponse(nitroApp.hooks, (event) => {
|
|
59
|
+
if (tracked.has(event))
|
|
60
|
+
yuva.waitUntil(event)?.(analytics.flush());
|
|
61
|
+
});
|
|
62
|
+
nitroApp.hooks.hook("close", () => analytics.close());
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export const IDENTITY_ROUTE = "/_yuva/identity";
|
|
5
|
+
export const WEBHOOK_ROUTE = "/_yuva/webhook";
|
|
6
|
+
export const SERVER_MODULE = "#yuva/server";
|
|
7
|
+
export function serverRuntimeConfig(current, options) {
|
|
8
|
+
return {
|
|
9
|
+
...current,
|
|
10
|
+
server: current?.server || options.server || "",
|
|
11
|
+
identitySecret: current?.identitySecret || options.identitySecret || "",
|
|
12
|
+
webhookSecret: current?.webhookSecret || options.webhookSecret || "",
|
|
13
|
+
analytics: {
|
|
14
|
+
...current?.analytics,
|
|
15
|
+
apiKey: current?.analytics?.apiKey || options.analytics?.apiKey || "",
|
|
16
|
+
propertyId: current?.analytics?.propertyId || options.analytics?.propertyId || ""
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function serverParts(config, env, prefix) {
|
|
21
|
+
const set = (value, name) => !!(value || env[`${prefix}YUVA_${name}`]);
|
|
22
|
+
return {
|
|
23
|
+
identity: set(config.identitySecret, "IDENTITY_SECRET"),
|
|
24
|
+
webhook: set(config.webhookSecret, "WEBHOOK_SECRET"),
|
|
25
|
+
analytics: set(config.analytics?.apiKey, "ANALYTICS_API_KEY") && set(config.analytics?.propertyId, "ANALYTICS_PROPERTY_ID")
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function findServerFile(dir) {
|
|
29
|
+
for (const ext of ["ts", "mts", "js", "mjs"]) {
|
|
30
|
+
const path = join(dir, `yuva.${ext}`);
|
|
31
|
+
if (existsSync(path))
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
export function runtimePath(name) {
|
|
37
|
+
return fileURLToPath(new URL(`./runtime/${name}.js`, import.meta.url));
|
|
38
|
+
}
|
|
39
|
+
export function serverModule({ nitro, server, runtimeConfig, h3 = "h3" }) {
|
|
40
|
+
const v3 = nitro >= 3;
|
|
41
|
+
const lines = [
|
|
42
|
+
`import { useRuntimeConfig } from ${JSON.stringify(runtimeConfig)}`,
|
|
43
|
+
...v3 ? [] : [`import { defineEventHandler, getRequestIP, getRequestURL, toWebRequest } from ${JSON.stringify(h3)}`],
|
|
44
|
+
server ? `import server from ${JSON.stringify(server)}` : "const server = {}",
|
|
45
|
+
"export { server }",
|
|
46
|
+
"export const config = () => useRuntimeConfig()",
|
|
47
|
+
...v3 ? [
|
|
48
|
+
"export const defineHandler = (handler) => handler",
|
|
49
|
+
"export const toRequest = (event) => event.req",
|
|
50
|
+
"export const trackRequest = (event) => event.req",
|
|
51
|
+
'export const onResponse = (hooks, fn) => hooks.hook("response", (_response, event) => fn(event))',
|
|
52
|
+
"export const waitUntil = (event) => (event.req.waitUntil ? (promise) => event.waitUntil(promise) : undefined)"
|
|
53
|
+
] : [
|
|
54
|
+
"export const defineHandler = defineEventHandler",
|
|
55
|
+
"export const toRequest = (event) => toWebRequest(event)",
|
|
56
|
+
"export const trackRequest = (event) =>",
|
|
57
|
+
" Object.assign(new Request(getRequestURL(event), { headers: event.headers }), { ip: getRequestIP(event) })",
|
|
58
|
+
'export const onResponse = (hooks, fn) => hooks.hook("afterResponse", (event) => fn(event))',
|
|
59
|
+
"export const waitUntil = (event) => (event.context.waitUntil ? (promise) => event.waitUntil(promise) : undefined)"
|
|
60
|
+
]
|
|
61
|
+
];
|
|
62
|
+
return `${lines.join(`
|
|
63
|
+
`)}
|
|
64
|
+
`;
|
|
65
|
+
}
|
package/dist/esm/nuxt/module.js
CHANGED
|
@@ -1,15 +1,55 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as kit from "@nuxt/kit";
|
|
2
|
+
import {
|
|
3
|
+
addComponent,
|
|
4
|
+
addImports,
|
|
5
|
+
addPlugin,
|
|
6
|
+
addServerHandler,
|
|
7
|
+
addServerImports,
|
|
8
|
+
addServerPlugin,
|
|
9
|
+
addTypeTemplate,
|
|
10
|
+
defineNuxtModule,
|
|
11
|
+
useLogger
|
|
12
|
+
} from "@nuxt/kit";
|
|
13
|
+
import {
|
|
14
|
+
findServerFile,
|
|
15
|
+
IDENTITY_ROUTE,
|
|
16
|
+
runtimePath,
|
|
17
|
+
SERVER_MODULE,
|
|
18
|
+
serverModule,
|
|
19
|
+
serverParts,
|
|
20
|
+
serverRuntimeConfig,
|
|
21
|
+
WEBHOOK_ROUTE
|
|
22
|
+
} from "../nitro/setup.js";
|
|
2
23
|
const components = ["YuvaChat", "YuvaDiscussions", "YuvaPageFeedback", "YuvaSubscribe"];
|
|
24
|
+
const serverTypes = `import type { H3Event } from "h3"
|
|
25
|
+
|
|
26
|
+
declare module "useyuva/nitro/runtime" {
|
|
27
|
+
interface YuvaServerTypes {
|
|
28
|
+
event: H3Event
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {}
|
|
33
|
+
`;
|
|
3
34
|
export default defineNuxtModule({
|
|
4
35
|
meta: { name: "useyuva", configKey: "yuva" },
|
|
5
36
|
defaults: { server: "", channel: "", locale: "" },
|
|
6
37
|
setup(options, nuxt) {
|
|
38
|
+
const { apiKey, propertyId, ...clientAnalytics } = options.analytics ?? { key: "" };
|
|
39
|
+
const secrets = serverRuntimeConfig(nuxt.options.runtimeConfig.yuva, {
|
|
40
|
+
identitySecret: options.identitySecret,
|
|
41
|
+
webhookSecret: options.webhookSecret,
|
|
42
|
+
analytics: { apiKey, propertyId }
|
|
43
|
+
});
|
|
44
|
+
const parts = serverParts(secrets, process.env, "NUXT_");
|
|
7
45
|
const current = nuxt.options.runtimeConfig.public.yuva ?? {};
|
|
46
|
+
const base = nuxt.options.app.baseURL.replace(/\/+$/, "");
|
|
8
47
|
nuxt.options.runtimeConfig.public.yuva = {
|
|
9
48
|
server: current.server || options.server || "",
|
|
10
49
|
channel: current.channel || options.channel || "",
|
|
11
50
|
locale: current.locale || options.locale || "",
|
|
12
|
-
|
|
51
|
+
identity: current.identity || (parts.identity ? `${base}${IDENTITY_ROUTE}` : ""),
|
|
52
|
+
analytics: current.analytics || (options.analytics ? clientAnalytics : undefined)
|
|
13
53
|
};
|
|
14
54
|
const compilerOptions = nuxt.options.vue.compilerOptions ??= {};
|
|
15
55
|
const isCustomElement = compilerOptions.isCustomElement;
|
|
@@ -20,5 +60,27 @@ export default defineNuxtModule({
|
|
|
20
60
|
addImports({ name: "useYuvaAnalytics", from: "useyuva/nuxt/analytics" });
|
|
21
61
|
if (nuxt.options.runtimeConfig.public.yuva.analytics)
|
|
22
62
|
addPlugin({ src: "useyuva/nuxt/plugin", mode: "client" });
|
|
63
|
+
addServerImports([{ name: "defineYuvaServer", from: "useyuva/nitro/runtime" }]);
|
|
64
|
+
addTypeTemplate({ filename: "types/useyuva-server.d.ts", getContents: () => serverTypes }, { nitro: true });
|
|
65
|
+
if (!parts.identity && !parts.webhook && !parts.analytics)
|
|
66
|
+
return;
|
|
67
|
+
nuxt.options.runtimeConfig.yuva = secrets;
|
|
68
|
+
const server = findServerFile(nuxt.options.serverDir);
|
|
69
|
+
if (!server && parts.identity) {
|
|
70
|
+
useLogger("useyuva").warn(`No yuva.ts in ${nuxt.options.serverDir}, so ${IDENTITY_ROUTE} answers 404`);
|
|
71
|
+
}
|
|
72
|
+
const nitro = kit.getNitroVersion?.(nuxt) ?? 2;
|
|
73
|
+
nuxt.hook("nitro:config", (config) => {
|
|
74
|
+
config.virtual = {
|
|
75
|
+
...config.virtual,
|
|
76
|
+
[SERVER_MODULE]: serverModule({ nitro, server, runtimeConfig: "#imports" })
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
if (parts.identity)
|
|
80
|
+
addServerHandler({ route: IDENTITY_ROUTE, method: "GET", handler: runtimePath("identity") });
|
|
81
|
+
if (parts.webhook)
|
|
82
|
+
addServerHandler({ route: WEBHOOK_ROUTE, method: "POST", handler: runtimePath("webhook") });
|
|
83
|
+
if (parts.analytics)
|
|
84
|
+
addServerPlugin(runtimePath("plugin"));
|
|
23
85
|
}
|
|
24
86
|
});
|
|
@@ -18,12 +18,19 @@ const subscribeProps = {
|
|
|
18
18
|
consent: String,
|
|
19
19
|
privacyUrl: String
|
|
20
20
|
};
|
|
21
|
+
function identityRoute(path) {
|
|
22
|
+
return async () => {
|
|
23
|
+
const response = await fetch(path, { credentials: "same-origin" });
|
|
24
|
+
return response.ok ? (await response.json()).token : null;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
21
27
|
function setup(tag, load, props, attributes, listeners) {
|
|
22
28
|
const config = useRuntimeConfig().public.yuva;
|
|
23
29
|
const element = ref(null);
|
|
30
|
+
const route = config?.identity ? identityRoute(config.identity) : undefined;
|
|
24
31
|
onMounted(() => {
|
|
25
32
|
load();
|
|
26
|
-
watch(() => props.identityToken, (source) => {
|
|
33
|
+
watch(() => props.identityToken === undefined ? route : props.identityToken, (source) => {
|
|
27
34
|
const target = element.value;
|
|
28
35
|
if (!target || typeof source !== "function")
|
|
29
36
|
return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DEFAULT_IDENTITY_TTL2,MAX_IDENTITY_TTL2,signIdentityToken2,DEFAULT_WEBHOOK_TOLERANCE2,WebhookVerificationError2,signWebhook2,verifyWebhookBody2,verifyWebhook2}from"./index-qcd538se.js";var MAX_BATCH=100;class AnalyticsEventError extends Error{event;detail;constructor(event,detail){super(`analytics: event ${JSON.stringify(event.name)} refused: ${detail}`);this.name="AnalyticsEventError",this.event=event,this.detail=detail}}class AnalyticsBatchError extends Error{status;body;events;constructor(events,status,body,cause){super(cause===void 0?`analytics: ${events} events not sent: ${status} ${body}`:`analytics: ${events} events not sent: ${cause instanceof Error?cause.message:String(cause)}`,cause===void 0?void 0:{cause});this.name="AnalyticsBatchError",this.status=status,this.body=body,this.events=events}}var QUEUE_FULL="analytics: the queue is full; event dropped",CLOSED="analytics: client closed",rfc3339=(time)=>time.toISOString().replace(/\.?0+Z$/,"Z");function wire(event){return{name:event.name,url:event.url,referrer:event.referrer||void 0,timestamp:rfc3339(event.time),props:event.props&&Object.keys(event.props).length>0?event.props:void 0,revenue:event.revenue?{a:event.revenue.amount,c:event.revenue.currency}:void 0,interactive:event.nonInteractive?!1:void 0,consent:event.consent||void 0,ip:event.ip||void 0,user_agent:event.userAgent||void 0,sec_ch_ua_platform:event.secChUaPlatform||void 0,sec_ch_ua_mobile:event.secChUaMobile||void 0,accept_language:event.acceptLanguage||void 0,visitor_key:event.visitorKey||void 0,persistent_id:event.persistentId||void 0,identity_token:event.identityToken||void 0}}function requestURL(request){let url=new URL(request.url);if(request.headers.get("x-forwarded-proto")?.toLowerCase()==="https")url.protocol="https:";return url.hash="",url.href}var socketIP=(request)=>{let ip=request.ip;return typeof ip==="string"?ip:void 0};function unref(timer){if(typeof timer==="object"&&timer&&"unref"in timer&&typeof timer.unref==="function")timer.unref();else globalThis.Deno?.unrefTimer?.(timer);return timer}var sleep=(ms,hold)=>new Promise((resolve)=>{let timer=setTimeout(resolve,ms);if(!hold)unref(timer)});function createAnalytics(config){if(!config.server||!config.apiKey||!config.propertyId)throw Error("analytics: server, apiKey and propertyId are required");let endpoint=`${config.server.replace(/\/+$/,"")}/v1/analytics/properties/${encodeURIComponent(config.propertyId)}/events`,interval=config.flushInterval&&config.flushInterval>0?config.flushInterval:5000,queueSize=config.queueSize&&config.queueSize>0?config.queueSize:1e4,maxRetries=config.maxRetries||5,minBackoff=config.minBackoff&&config.minBackoff>0?config.minBackoff:1000,clientIP=config.clientIP??socketIP,queue=[],closed=!1,timer,chain=Promise.resolve(),report=(error)=>{try{config.onError?.(error)}catch{}};async function post(body){let res=await(config.fetch??fetch)(endpoint,{method:"POST",headers:{Authorization:`Bearer ${config.apiKey}`,"Content-Type":"application/json"},body}),raw=(await res.text()).slice(0,65536).trim(),seconds=Number(res.headers.get("retry-after"));return{status:res.status,raw,retryAfter:Number.isInteger(seconds)&&seconds>0?seconds*1000:0}}async function sendBatch(batch,hold){let body=JSON.stringify({events:batch.map(wire)}),backoff=minBackoff;for(let attempt=0;;attempt++){let res=await post(body).catch((error)=>({error}));if(!("error"in res)){if(res.status===202){try{let result=JSON.parse(res.raw);for(let{index,detail}of result.errors??[]){let event=batch[index];if(event)report(new AnalyticsEventError(event,detail))}}catch{}return}if(res.status!==429&&res.status<500)return new AnalyticsBatchError(batch.length,res.status,res.raw)}if(attempt>=maxRetries)return"error"in res?new AnalyticsBatchError(batch.length,0,"",res.error):new AnalyticsBatchError(batch.length,res.status,res.raw);await sleep(Math.max(backoff,"error"in res?0:res.retryAfter),hold),backoff=Math.min(backoff*2,60000)}}async function sendAll(hold){let last;for(;;){let batch=queue.slice(0,100);if(queue=queue.slice(batch.length),batch.length===0)return last;let error=await sendBatch(batch,hold);if(error)last=error,report(error)}}let run=(hold)=>{let next=chain.then(()=>sendAll(hold));return chain=next,next};function send(event){if(!event.name||!event.url)throw Error("analytics: an event needs a name and a url");if(closed)throw Error(CLOSED);if(queue.length>=queueSize){report(Error(QUEUE_FULL));return}if(queue.push({...event,time:event.time??new Date}),queue.length>=100)run(!1);else if(!timer)timer=unref(setTimeout(()=>{timer=void 0,run(!1)},interval))}function track(request,event){let headers=request.headers;if(headers.get("dnt")==="1"||headers.get("sec-gpc")==="1")return;let e={...event};if(!e.visitorKey){if(!e.ip)e.ip=clientIP(request)??void 0;if(!e.userAgent)e.userAgent=headers.get("user-agent")??void 0,e.secChUaPlatform=headers.get("sec-ch-ua-platform")??void 0,e.secChUaMobile=headers.get("sec-ch-ua-mobile")??void 0}if(!e.acceptLanguage)e.acceptLanguage=headers.get("accept-language")??void 0;if(!e.url)e.url=requestURL(request);if(!e.referrer)e.referrer=headers.get("referer")??void 0;send(e)}return{track,send,flush:()=>run(!0),async close(){if(closed)return;if(closed=!0,timer)clearTimeout(timer);return timer=void 0,run(!0)}}}export{AnalyticsBatchError,AnalyticsEventError,DEFAULT_IDENTITY_TTL2 as DEFAULT_IDENTITY_TTL,DEFAULT_WEBHOOK_TOLERANCE2 as DEFAULT_WEBHOOK_TOLERANCE,MAX_BATCH,MAX_IDENTITY_TTL2 as MAX_IDENTITY_TTL,WebhookVerificationError2 as WebhookVerificationError,createAnalytics,signIdentityToken2 as signIdentityToken,signWebhook2 as signWebhook,verifyWebhook2 as verifyWebhook,verifyWebhookBody2 as verifyWebhookBody};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{inlineNodes}from"./index-jdxce2se.js";import{el}from"./index-rdzxvj2r.js";import{translate}from"./index-r9f9ttb2.js";var messages=JSON.parse('{"2Hlmdt":["Write a reply…"],"3CApMv":["All discussions"],"3PqJQG":["Nothing to preview."],"4-S4QM":["Unmark as answer"],"5lWFkC":["Sign in"],"8wRl7c":["The opening post was deleted."],"99w-GZ":["Sign in to start a discussion. Your name is shown with your post."],"F_vGso":["Your reply"],"HkXOWm":["This reply was deleted."],"Hw3P48":["Replies: ",["0"]],"ImOQa9":["Reply"],"JfAIiv":["This discussion is locked. New replies are closed."],"Kl3TuI":["Get an e-mail when someone replies or an answer is accepted."],"Klsidc":["Delete this reply?"],"MHrjPM":["Title"],"MKEPCY":["Follow"],"NjUiZ4":["Ask a question, share an idea or describe a problem with this page."],"NrGspR":["Thanks! Your discussion will appear for everyone once the team approves it."],"OVijcR":["Remove your vote (",["0"]," votes)"],"PL0621":["Sign in to reply, vote and follow this discussion."],"PN38hZ":["Your question or idea"],"PZ_kxo":["Keep"],"Pwqkdw":["Loading…"],"RqsQKK":["Your first posts are checked by the team before they appear for everyone."],"Sad2tK":["Sending…"],"VGqKyv":["This discussion is no longer available."],"Vwy_nT":["Markdown is supported."],"YZayoP":["Thanks! Your reply will appear for everyone once the team approves it."],"cnGeoo":["Delete"],"dEgA5A":["Cancel"],"dNOvhg":["Upvote (",["0"]," votes)"],"ePK91l":["Edit"],"i4_LY_":["Write"],"jhu92n":["Mark as answer"],"k4b5_X":["edited"],"n5vqdp":["Start discussion"],"qzVesX":["Edit your post"],"rAl91-":["Delete this discussion?"],"rdUucN":["Preview"],"tfDRzk":["Save"],"y-KXxN":["Accepted answer"],"y6sq5j":["Following"],"z1qf6l":["Start a discussion"]}');var messages2=JSON.parse('{"2Hlmdt":["Antwort schreiben…"],"3CApMv":["Alle Diskussionen"],"3PqJQG":["Noch nichts für die Vorschau."],"4-S4QM":["Antwortmarkierung entfernen"],"5lWFkC":["Anmelden"],"8wRl7c":["Der erste Beitrag wurde gelöscht."],"99w-GZ":["Melden Sie sich an, um eine Diskussion zu starten. Ihr Name wird mit Ihrem Beitrag angezeigt."],"F_vGso":["Ihre Antwort"],"HkXOWm":["Diese Antwort wurde gelöscht."],"Hw3P48":["Antworten: ",["0"]],"ImOQa9":["Antworten"],"JfAIiv":["Diese Diskussion ist gesperrt. Neue Antworten sind nicht möglich."],"Kl3TuI":["Erhalten Sie eine E-Mail, wenn jemand antwortet oder eine Antwort akzeptiert wird."],"Klsidc":["Diese Antwort löschen?"],"MHrjPM":["Titel"],"MKEPCY":["Folgen"],"NjUiZ4":["Stellen Sie eine Frage, teilen Sie eine Idee oder beschreiben Sie ein Problem mit dieser Seite."],"NrGspR":["Danke! Ihre Diskussion ist für alle sichtbar, sobald das Team sie freigibt."],"OVijcR":["Stimme zurückziehen (",["0"]," Stimmen)"],"PL0621":["Melden Sie sich an, um zu antworten, abzustimmen und dieser Diskussion zu folgen."],"PN38hZ":["Ihre Frage oder Idee"],"PZ_kxo":["Behalten"],"Pwqkdw":["Wird geladen…"],"RqsQKK":["Ihre ersten Beiträge prüft das Team, bevor sie für alle sichtbar sind."],"Sad2tK":["Wird gesendet…"],"VGqKyv":["Diese Diskussion ist nicht mehr verfügbar."],"Vwy_nT":["Markdown wird unterstützt."],"YZayoP":["Danke! Ihre Antwort ist für alle sichtbar, sobald das Team sie freigibt."],"cnGeoo":["Löschen"],"dEgA5A":["Abbrechen"],"dNOvhg":["Zustimmen (",["0"]," Stimmen)"],"ePK91l":["Bearbeiten"],"i4_LY_":["Schreiben"],"jhu92n":["Als Antwort markieren"],"k4b5_X":["bearbeitet"],"n5vqdp":["Diskussion starten"],"qzVesX":["Beitrag bearbeiten"],"rAl91-":["Diese Diskussion löschen?"],"rdUucN":["Vorschau"],"tfDRzk":["Speichern"],"y-KXxN":["Akzeptierte Antwort"],"y6sq5j":["Gefolgt"],"z1qf6l":["Diskussion starten"]}');var messages3=JSON.parse('{"2Hlmdt":["Bir yanıt yazın…"],"3CApMv":["Tüm tartışmalar"],"3PqJQG":["Önizlenecek bir şey yok."],"4-S4QM":["Yanıt işaretini kaldır"],"5lWFkC":["Giriş yap"],"8wRl7c":["Açılış gönderisi silindi."],"99w-GZ":["Tartışma başlatmak için giriş yapın. Adınız gönderinizle birlikte gösterilir."],"F_vGso":["Yanıtınız"],"HkXOWm":["Bu yanıt silindi."],"Hw3P48":["Yanıtlar: ",["0"]],"ImOQa9":["Yanıtla"],"JfAIiv":["Bu tartışma kilitli. Yeni yanıt yazılamaz."],"Kl3TuI":["Biri yanıt yazdığında ya da bir yanıt kabul edildiğinde e-posta alın."],"Klsidc":["Bu yanıt silinsin mi?"],"MHrjPM":["Başlık"],"MKEPCY":["Takip et"],"NjUiZ4":["Bir soru sorun, bir fikir paylaşın ya da bu sayfadaki bir sorunu anlatın."],"NrGspR":["Teşekkürler! Tartışmanız ekip onayladığında herkese görünecek."],"OVijcR":["Oyunuzu geri alın (",["0"]," oy)"],"PL0621":["Bu tartışmada yanıt yazmak, oy vermek ve takip etmek için giriş yapın."],"PN38hZ":["Sorunuz ya da fikriniz"],"PZ_kxo":["Kalsın"],"Pwqkdw":["Yükleniyor…"],"RqsQKK":["İlk gönderileriniz herkese görünmeden önce ekip tarafından kontrol edilir."],"Sad2tK":["Gönderiliyor…"],"VGqKyv":["Bu tartışma artık mevcut değil."],"Vwy_nT":["Markdown desteklenir."],"YZayoP":["Teşekkürler! Yanıtınız ekip onayladığında herkese görünecek."],"cnGeoo":["Sil"],"dEgA5A":["Vazgeç"],"dNOvhg":["Oy ver (",["0"]," oy)"],"ePK91l":["Düzenle"],"i4_LY_":["Yaz"],"jhu92n":["Yanıt olarak işaretle"],"k4b5_X":["düzenlendi"],"n5vqdp":["Tartışmayı başlat"],"qzVesX":["Gönderinizi düzenleyin"],"rAl91-":["Bu tartışma silinsin mi?"],"rdUucN":["Önizleme"],"tfDRzk":["Kaydet"],"y-KXxN":["Kabul edilen yanıt"],"y6sq5j":["Takip ediliyor"],"z1qf6l":["Tartışma başlat"]}');var linkPattern=/\[([^\]\n]+)\]\(([^)\s]+)\)/g;function inline(text){let nodes=[],last=0;for(let match of text.matchAll(linkPattern)){let index=match.index??0;if(index>last)nodes.push(...inlineNodes(text.slice(last,index)));let[whole,label="",href=""]=match,url=null;try{url=new URL(href)}catch{}if(url&&(url.protocol==="http:"||url.protocol==="https:")){let a=document.createElement("a");a.href=url.href,a.target="_blank",a.rel="nofollow ugc noopener",a.append(...inlineNodes(label)),nodes.push(a)}else nodes.push(document.createTextNode(whole));last=index+whole.length}if(last<text.length)nodes.push(...inlineNodes(text.slice(last)));return nodes}function block(tag,lines){let element=document.createElement(tag);return element.append(...inline(lines.join(" "))),element}function preview(markdown){let fragment=document.createDocumentFragment(),lines=markdown.replace(/\r\n?/g,`
|
|
2
2
|
`).split(`
|
|
3
3
|
`),paragraph=[],list=null,quote=[],flush=()=>{if(paragraph.length)fragment.append(block("p",paragraph));if(quote.length){let element=document.createElement("blockquote");element.append(preview(quote.join(`
|
|
4
4
|
`))),fragment.append(element)}paragraph=[],quote=[],list=null};for(let i=0;i<lines.length;i++){let line=lines[i]??"",fence=/^\s*(```|~~~)/.exec(line);if(fence){flush();let code=[];while(++i<lines.length&&!(lines[i]??"").trimStart().startsWith(fence[1]??"```"))code.push(lines[i]??"");let pre=document.createElement("pre");pre.append(Object.assign(document.createElement("code"),{textContent:code.join(`
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type EventHandlerRequest, type EventHandlerWithFetch, type H3Event, type Middleware } from "h3";
|
|
2
|
+
import type { ServerAnalytics, ServerEvent } from "./server/analytics.js";
|
|
3
|
+
import { type IdentityClaims } from "./server/identity.js";
|
|
4
|
+
import { type WebhookEvent, type WebhookEventType } from "./server/webhook.js";
|
|
5
|
+
export interface YuvaContext {
|
|
6
|
+
track(event: ServerEvent): void;
|
|
7
|
+
}
|
|
8
|
+
declare module "h3" {
|
|
9
|
+
interface H3EventContext {
|
|
10
|
+
yuva?: YuvaContext;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
14
|
+
export interface YuvaIdentityHandlerOptions {
|
|
15
|
+
secret: string;
|
|
16
|
+
user: (event: H3Event) => MaybePromise<IdentityClaims | null | undefined>;
|
|
17
|
+
ttl?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function defineYuvaIdentityHandler({ secret, user, ttl, }: YuvaIdentityHandlerOptions): EventHandlerWithFetch<EventHandlerRequest, Promise<Response>>;
|
|
20
|
+
export type WebhookPayload<K extends WebhookEventType> = Extract<WebhookEvent, {
|
|
21
|
+
type: K;
|
|
22
|
+
}>;
|
|
23
|
+
export type YuvaWebhookHandlers = {
|
|
24
|
+
[K in WebhookEventType]?: (payload: WebhookPayload<K>, event: H3Event) => unknown;
|
|
25
|
+
};
|
|
26
|
+
export interface YuvaWebhookHandlerOptions {
|
|
27
|
+
secret: string;
|
|
28
|
+
tolerance?: number;
|
|
29
|
+
on: YuvaWebhookHandlers;
|
|
30
|
+
}
|
|
31
|
+
export declare function defineYuvaWebhookHandler({ secret, tolerance, on, }: YuvaWebhookHandlerOptions): EventHandlerWithFetch<EventHandlerRequest, Promise<Response>>;
|
|
32
|
+
export interface YuvaAnalyticsOptions {
|
|
33
|
+
analytics: ServerAnalytics;
|
|
34
|
+
}
|
|
35
|
+
export declare function defineYuvaAnalytics({ analytics }: YuvaAnalyticsOptions): Middleware;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NitroModule } from "nitro/types";
|
|
2
|
+
import { type YuvaServerOptions } from "./setup.js";
|
|
3
|
+
export interface YuvaNitroOptions extends YuvaServerOptions {
|
|
4
|
+
server?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function yuva(options?: YuvaNitroOptions): NitroModule;
|
|
7
|
+
declare const _default: NitroModule;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { YuvaServer } from "./define.js";
|
|
2
|
+
export interface YuvaServerRuntimeConfig {
|
|
3
|
+
server?: string;
|
|
4
|
+
identitySecret?: string;
|
|
5
|
+
webhookSecret?: string;
|
|
6
|
+
analytics?: {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
propertyId?: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface YuvaRuntimeConfig {
|
|
12
|
+
yuva?: YuvaServerRuntimeConfig;
|
|
13
|
+
public?: {
|
|
14
|
+
yuva?: {
|
|
15
|
+
server?: string;
|
|
16
|
+
analytics?: {
|
|
17
|
+
server?: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface YuvaHookEvent {
|
|
23
|
+
context: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface YuvaHooks {
|
|
26
|
+
hook(name: string, fn: (...args: never[]) => unknown): unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface YuvaAdapter {
|
|
29
|
+
server: YuvaServer<YuvaHookEvent>;
|
|
30
|
+
config(): YuvaRuntimeConfig;
|
|
31
|
+
defineHandler(handler: (event: YuvaHookEvent) => Promise<Response>): unknown;
|
|
32
|
+
toRequest(event: YuvaHookEvent): Request;
|
|
33
|
+
trackRequest(event: YuvaHookEvent): Request;
|
|
34
|
+
onResponse(hooks: YuvaHooks, fn: (event: YuvaHookEvent) => void): void;
|
|
35
|
+
waitUntil(event: YuvaHookEvent): ((promise: Promise<unknown>) => void) | undefined;
|
|
36
|
+
}
|
|
37
|
+
export declare function identityHandler(yuva: YuvaAdapter): unknown;
|
|
38
|
+
export declare function webhookHandler(yuva: YuvaAdapter): unknown;
|
|
39
|
+
export declare function analyticsPlugin(yuva: YuvaAdapter): (nitroApp: {
|
|
40
|
+
hooks: YuvaHooks;
|
|
41
|
+
}) => void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IdentityClaims, ServerEvent, WebhookEvent, WebhookEventType } from "../../server.js";
|
|
2
|
+
export interface YuvaServerTypes {
|
|
3
|
+
}
|
|
4
|
+
export type YuvaServerEvent = YuvaServerTypes extends {
|
|
5
|
+
event: infer E;
|
|
6
|
+
} ? E : import("h3").H3Event;
|
|
7
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
8
|
+
export type YuvaServerWebhooks<E = YuvaServerEvent> = {
|
|
9
|
+
[K in WebhookEventType]?: (payload: Extract<WebhookEvent, {
|
|
10
|
+
type: K;
|
|
11
|
+
}>, event: E) => unknown;
|
|
12
|
+
};
|
|
13
|
+
export interface YuvaServer<E = YuvaServerEvent> {
|
|
14
|
+
user?: (event: E) => MaybePromise<IdentityClaims | null | undefined>;
|
|
15
|
+
webhooks?: YuvaServerWebhooks<E>;
|
|
16
|
+
}
|
|
17
|
+
export interface YuvaContext {
|
|
18
|
+
track(event: ServerEvent): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function defineYuvaServer<E = YuvaServerEvent>(server: YuvaServer<E>): YuvaServer<E>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { YuvaServerRuntimeConfig } from "./runtime/core.js";
|
|
2
|
+
export type { YuvaServerRuntimeConfig };
|
|
3
|
+
export declare const IDENTITY_ROUTE = "/_yuva/identity";
|
|
4
|
+
export declare const WEBHOOK_ROUTE = "/_yuva/webhook";
|
|
5
|
+
export declare const SERVER_MODULE = "#yuva/server";
|
|
6
|
+
export interface YuvaServerOptions {
|
|
7
|
+
identitySecret?: string;
|
|
8
|
+
webhookSecret?: string;
|
|
9
|
+
analytics?: {
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
propertyId?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface YuvaServerParts {
|
|
15
|
+
identity: boolean;
|
|
16
|
+
webhook: boolean;
|
|
17
|
+
analytics: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function serverRuntimeConfig(current: YuvaServerRuntimeConfig | undefined, options: YuvaServerOptions & {
|
|
20
|
+
server?: string;
|
|
21
|
+
}): Required<Omit<YuvaServerRuntimeConfig, "analytics">> & {
|
|
22
|
+
analytics: {
|
|
23
|
+
apiKey: string;
|
|
24
|
+
propertyId: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export declare function serverParts(config: YuvaServerRuntimeConfig, env: Record<string, string | undefined>, prefix: string): YuvaServerParts;
|
|
28
|
+
export declare function findServerFile(dir: string): string | undefined;
|
|
29
|
+
export declare function runtimePath(name: "identity" | "webhook" | "plugin"): string;
|
|
30
|
+
export interface ServerModuleOptions {
|
|
31
|
+
nitro: number;
|
|
32
|
+
server: string | undefined;
|
|
33
|
+
runtimeConfig: string;
|
|
34
|
+
h3?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function serverModule({ nitro, server, runtimeConfig, h3 }: ServerModuleOptions): string;
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import type { AnalyticsOptions } from "../analytics.js";
|
|
2
|
+
import { type YuvaServerRuntimeConfig } from "../nitro/setup.js";
|
|
2
3
|
export interface AnalyticsModuleOptions extends Omit<AnalyticsOptions, "server"> {
|
|
3
4
|
server?: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
propertyId?: string;
|
|
4
7
|
}
|
|
8
|
+
export type PublicAnalyticsOptions = Omit<AnalyticsModuleOptions, "apiKey" | "propertyId">;
|
|
5
9
|
export interface ModuleOptions {
|
|
6
10
|
server?: string;
|
|
7
11
|
channel?: string;
|
|
8
12
|
locale?: string;
|
|
9
13
|
analytics?: AnalyticsModuleOptions;
|
|
14
|
+
identitySecret?: string;
|
|
15
|
+
webhookSecret?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PublicModuleConfig {
|
|
18
|
+
server: string;
|
|
19
|
+
channel: string;
|
|
20
|
+
locale: string;
|
|
21
|
+
identity: string;
|
|
22
|
+
analytics?: PublicAnalyticsOptions;
|
|
10
23
|
}
|
|
11
24
|
declare module "@nuxt/schema" {
|
|
12
25
|
interface PublicRuntimeConfig {
|
|
13
|
-
yuva:
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
yuva: PublicModuleConfig;
|
|
27
|
+
}
|
|
28
|
+
interface RuntimeConfig {
|
|
29
|
+
yuva?: YuvaServerRuntimeConfig;
|
|
16
30
|
}
|
|
17
31
|
}
|
|
18
32
|
declare const _default: import("@nuxt/schema").NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export declare const MAX_BATCH = 100;
|
|
2
|
+
export interface ServerAnalyticsConfig {
|
|
3
|
+
server: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
propertyId: string;
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
flushInterval?: number;
|
|
8
|
+
queueSize?: number;
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
minBackoff?: number;
|
|
11
|
+
clientIP?: (request: Request) => string | null | undefined;
|
|
12
|
+
onError?: (error: Error) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface ServerEvent {
|
|
15
|
+
name: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
referrer?: string;
|
|
18
|
+
time?: Date;
|
|
19
|
+
props?: Record<string, string | number | boolean>;
|
|
20
|
+
revenue?: {
|
|
21
|
+
amount: string;
|
|
22
|
+
currency: string;
|
|
23
|
+
};
|
|
24
|
+
nonInteractive?: boolean;
|
|
25
|
+
consent?: boolean;
|
|
26
|
+
ip?: string;
|
|
27
|
+
userAgent?: string;
|
|
28
|
+
secChUaPlatform?: string;
|
|
29
|
+
secChUaMobile?: string;
|
|
30
|
+
acceptLanguage?: string;
|
|
31
|
+
visitorKey?: string;
|
|
32
|
+
persistentId?: string;
|
|
33
|
+
identityToken?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ServerAnalytics {
|
|
36
|
+
track(request: Request, event: ServerEvent): void;
|
|
37
|
+
send(event: ServerEvent): void;
|
|
38
|
+
flush(): Promise<Error | undefined>;
|
|
39
|
+
close(): Promise<Error | undefined>;
|
|
40
|
+
}
|
|
41
|
+
export declare class AnalyticsEventError extends Error {
|
|
42
|
+
readonly event: ServerEvent;
|
|
43
|
+
readonly detail: string;
|
|
44
|
+
constructor(event: ServerEvent, detail: string);
|
|
45
|
+
}
|
|
46
|
+
export declare class AnalyticsBatchError extends Error {
|
|
47
|
+
readonly status: number;
|
|
48
|
+
readonly body: string;
|
|
49
|
+
readonly events: number;
|
|
50
|
+
constructor(events: number, status: number, body: string, cause?: unknown);
|
|
51
|
+
}
|
|
52
|
+
export declare function createAnalytics(config: ServerAnalyticsConfig): ServerAnalytics;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const utf8: (text: string) => Uint8Array<ArrayBuffer>;
|
|
2
|
+
export declare function base64(bytes: Uint8Array): string;
|
|
3
|
+
export declare const base64url: (bytes: Uint8Array) => string;
|
|
4
|
+
export declare function decodeBase64(text: string): Uint8Array<ArrayBuffer> | undefined;
|
|
5
|
+
export declare const hmacKey: (key: Uint8Array<ArrayBuffer>, usages: KeyUsage[]) => Promise<CryptoKey>;
|
|
6
|
+
export declare function goJSON(value: unknown): string | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const DEFAULT_IDENTITY_TTL = 300;
|
|
2
|
+
export declare const MAX_IDENTITY_TTL = 600;
|
|
3
|
+
export interface IdentityClaims {
|
|
4
|
+
subject: string;
|
|
5
|
+
email?: string;
|
|
6
|
+
emailVerified?: boolean;
|
|
7
|
+
name?: string;
|
|
8
|
+
locale?: string;
|
|
9
|
+
attrs?: Record<string, unknown>;
|
|
10
|
+
id?: string;
|
|
11
|
+
issuedAt?: Date;
|
|
12
|
+
ttl?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function signIdentityToken(secret: string, claims: IdentityClaims): Promise<string>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { webhooks } from "../schema.gen.js";
|
|
2
|
+
type Payload<K extends keyof webhooks> = webhooks[K]["post"] extends {
|
|
3
|
+
requestBody: {
|
|
4
|
+
content: {
|
|
5
|
+
"application/json": infer Body;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
} ? Body : never;
|
|
9
|
+
export type WebhookEventType = keyof webhooks;
|
|
10
|
+
export type WebhookEvent = {
|
|
11
|
+
[K in WebhookEventType]: Omit<Payload<K>, "type"> & {
|
|
12
|
+
type: K;
|
|
13
|
+
};
|
|
14
|
+
}[WebhookEventType];
|
|
15
|
+
export declare const DEFAULT_WEBHOOK_TOLERANCE = 300;
|
|
16
|
+
export type WebhookErrorCode = "invalid_secret" | "missing_headers" | "invalid_timestamp" | "timestamp_too_old" | "timestamp_too_new" | "invalid_signature";
|
|
17
|
+
export declare class WebhookVerificationError extends Error {
|
|
18
|
+
readonly code: WebhookErrorCode;
|
|
19
|
+
constructor(code: WebhookErrorCode);
|
|
20
|
+
}
|
|
21
|
+
export interface VerifyWebhookOptions {
|
|
22
|
+
tolerance?: number;
|
|
23
|
+
now?: Date;
|
|
24
|
+
}
|
|
25
|
+
export declare function signWebhook(secret: string, id: string, timestamp: Date, body: string | Uint8Array): Promise<string>;
|
|
26
|
+
export declare function verifyWebhookBody(secret: string, headers: Headers, body: Uint8Array, { tolerance, now }?: VerifyWebhookOptions): Promise<void>;
|
|
27
|
+
export declare function verifyWebhook<T = WebhookEvent>(secret: string, request: Request, options?: VerifyWebhookOptions): Promise<T>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { DEFAULT_IDENTITY_TTL, MAX_IDENTITY_TTL, signIdentityToken, type IdentityClaims } from "./server/identity.js";
|
|
2
|
+
export { DEFAULT_WEBHOOK_TOLERANCE, signWebhook, verifyWebhook, verifyWebhookBody, WebhookVerificationError, type VerifyWebhookOptions, type WebhookErrorCode, type WebhookEvent, type WebhookEventType, } from "./server/webhook.js";
|
|
3
|
+
export { AnalyticsBatchError, AnalyticsEventError, createAnalytics, MAX_BATCH, type ServerAnalytics, type ServerAnalyticsConfig, type ServerEvent, } from "./server/analytics.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "useyuva",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Yuva for the web: a headless client for contacts, the <yuva-chat> element, documentation page feedback and discussions, the <yuva-subscribe> mailing list form, cookieless web analytics, React hooks, a Nuxt module
|
|
3
|
+
"version": "0.0.14",
|
|
4
|
+
"description": "Yuva for the web: a headless client for contacts, the <yuva-chat> element, documentation page feedback and discussions, the <yuva-subscribe> mailing list form, cookieless web analytics, React hooks, a Nuxt module, a typed /v1 client and server helpers for identity tokens, webhooks and server-side analytics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"web-component",
|
|
18
18
|
"customer-messaging",
|
|
19
19
|
"nuxt",
|
|
20
|
+
"nitro",
|
|
20
21
|
"newsletter",
|
|
21
22
|
"analytics"
|
|
22
23
|
],
|
|
@@ -61,6 +62,14 @@
|
|
|
61
62
|
"types": "./dist/types/api.d.ts",
|
|
62
63
|
"default": "./dist/esm/api.js"
|
|
63
64
|
},
|
|
65
|
+
"./server": {
|
|
66
|
+
"types": "./dist/types/server.d.ts",
|
|
67
|
+
"default": "./dist/esm/server.js"
|
|
68
|
+
},
|
|
69
|
+
"./h3": {
|
|
70
|
+
"types": "./dist/types/h3.d.ts",
|
|
71
|
+
"default": "./dist/esm/h3.js"
|
|
72
|
+
},
|
|
64
73
|
"./nuxt": {
|
|
65
74
|
"types": "./dist/types/nuxt/module.d.ts",
|
|
66
75
|
"default": "./dist/esm/nuxt/module.js"
|
|
@@ -77,6 +86,14 @@
|
|
|
77
86
|
"types": "./dist/types/nuxt/runtime/plugin.d.ts",
|
|
78
87
|
"default": "./dist/esm/nuxt/runtime/plugin.js"
|
|
79
88
|
},
|
|
89
|
+
"./nitro": {
|
|
90
|
+
"types": "./dist/types/nitro/module.d.ts",
|
|
91
|
+
"default": "./dist/esm/nitro/module.js"
|
|
92
|
+
},
|
|
93
|
+
"./nitro/runtime": {
|
|
94
|
+
"types": "./dist/types/nitro/runtime/define.d.ts",
|
|
95
|
+
"default": "./dist/esm/nitro/runtime/define.js"
|
|
96
|
+
},
|
|
80
97
|
"./package.json": "./package.json"
|
|
81
98
|
},
|
|
82
99
|
"types": "./dist/types/index.d.ts",
|
|
@@ -105,6 +122,7 @@
|
|
|
105
122
|
},
|
|
106
123
|
"peerDependencies": {
|
|
107
124
|
"@nuxt/kit": ">=3.15.0",
|
|
125
|
+
"h3": "^2.0.0",
|
|
108
126
|
"react": ">=18",
|
|
109
127
|
"vue": ">=3.5.0"
|
|
110
128
|
},
|
|
@@ -112,6 +130,9 @@
|
|
|
112
130
|
"@nuxt/kit": {
|
|
113
131
|
"optional": true
|
|
114
132
|
},
|
|
133
|
+
"h3": {
|
|
134
|
+
"optional": true
|
|
135
|
+
},
|
|
115
136
|
"react": {
|
|
116
137
|
"optional": true
|
|
117
138
|
},
|
|
@@ -130,7 +151,10 @@
|
|
|
130
151
|
"@types/bun": "^1.4.2",
|
|
131
152
|
"@types/react": "19.2.2",
|
|
132
153
|
"@types/react-dom": "19.2.2",
|
|
154
|
+
"h3": "2.0.2",
|
|
155
|
+
"h3-v1": "npm:h3@1.15.11",
|
|
133
156
|
"happy-dom": "^20.14.5",
|
|
157
|
+
"nitro": "3.0.260903-beta",
|
|
134
158
|
"oxfmt": "0.72.0",
|
|
135
159
|
"oxlint": "1.87.0",
|
|
136
160
|
"react": "19.2.0",
|