@usepapier/browser 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,49 @@
1
+ Papier SDK License
2
+
3
+ Copyright (c) 2026 Papier. All rights reserved.
4
+
5
+ This license applies to the Papier software packages that carry it, including
6
+ @usepapier/client, @usepapier/browser and @usepapier/cli (the "Software").
7
+
8
+ 1. Grant. Subject to these terms, Papier grants you a non-exclusive,
9
+ non-transferable, revocable, royalty-free license to install and use the
10
+ Software, and to include unmodified copies of it in applications you build,
11
+ solely to access and use the Papier service (https://usepapier.io) under a
12
+ valid Papier account and the Papier terms of service.
13
+
14
+ 2. Redistribution in your applications. You may distribute the Software as part
15
+ of your own application, in object or bundled form, provided the application
16
+ is not itself a software development kit, library or service that exposes or
17
+ substitutes for the Software or the Papier service.
18
+
19
+ 3. Restrictions. You may not: (a) copy, modify, or create derivative works of
20
+ the Software, except to the extent applicable law grants that right
21
+ regardless of this restriction; (b) use the Software, or any interface it
22
+ documents, to build or operate a product or service that competes with
23
+ Papier or that emulates the Papier service; (c) sell, sublicense, rent, or
24
+ offer the Software as a standalone product; (d) remove or alter copyright,
25
+ license or attribution notices; (e) use the Software to circumvent Papier
26
+ plan limits, quotas, authentication, or other access controls.
27
+
28
+ 4. Third-party components. The Software may include or depend on third-party
29
+ components that are licensed under their own terms. Those terms apply to
30
+ those components, and nothing in this license restricts rights granted by
31
+ them.
32
+
33
+ 5. Ownership. Papier and its licensors retain all right, title and interest in
34
+ the Software. No rights are granted except those expressly stated here.
35
+
36
+ 6. Termination. This license ends automatically if you breach it or when your
37
+ right to use the Papier service ends. On termination you must stop using the
38
+ Software and delete your copies.
39
+
40
+ 7. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
41
+ EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
42
+ PARTICULAR PURPOSE AND NON-INFRINGEMENT.
43
+
44
+ 8. Limitation of liability. TO THE MAXIMUM EXTENT PERMITTED BY LAW, PAPIER IS
45
+ NOT LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES, OR FOR
46
+ LOSS OF DATA OR PROFITS, ARISING FROM USE OF THE SOFTWARE. Use of the Papier
47
+ service is governed separately by the Papier terms of service.
48
+
49
+ Questions: https://usepapier.io
package/README.md CHANGED
@@ -1,3 +1,71 @@
1
- # Temporary Holding Version
1
+ # @usepapier/browser
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Web Push for the browser, using a **publishable key**. It registers and removes push subscriptions
4
+ from the origins you allow and cannot send anything. Sending happens on your server with
5
+ [`@usepapier/client`](https://www.npmjs.com/package/@usepapier/client).
6
+
7
+ - No credentials in the page: the key is public and limited to one project, one environment and your
8
+ allowed origins.
9
+ - Binds a device to a signed-in user only with a short-lived token your server issues.
10
+ - Handles permission, subscription, push-key rotation and unsupported browsers with an explicit state.
11
+ - Small, dependency-free, ESM.
12
+
13
+ Documentation: <https://usepapier.io/docs/>
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pnpm add @usepapier/browser
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```ts
24
+ import { createPapierBrowser } from "@usepapier/browser";
25
+
26
+ const papier = createPapierBrowser({ publishableKey: "papier_pk_..." });
27
+
28
+ // 1. Ask your server for a short-lived token that binds this browser to the signed-in user.
29
+ // (Server: `await papier.push.issueUserToken({ userId })`.)
30
+ // 2. Subscribe from a user gesture, such as a button click.
31
+ button.addEventListener("click", async () => {
32
+ const { token } = await fetch("/api/papier/push-token", { method: "POST" }).then((r) => r.json());
33
+ await papier.push.subscribe({ userToken: token });
34
+ });
35
+
36
+ // 3. Keep the subscription current on page load (handles push key rotation).
37
+ await papier.push.refresh();
38
+ ```
39
+
40
+ ## Service worker
41
+
42
+ Copy `node_modules/@usepapier/browser/build/service-worker.iife.js` to a file served from your
43
+ site's root as `/papier-sw.js` (or pass `serviceWorkerPath`). It shows the notification and opens
44
+ only same-origin links.
45
+
46
+ ## Setup in Papier
47
+
48
+ 1. Create a **Browser (publishable)** API key for one project and environment, and list your site's
49
+ origins (`https://app.example.com`; `http://localhost:3000` for development).
50
+ 2. Enable push delivery through Papier for that environment.
51
+ 3. Send from your server with a secret key.
52
+
53
+ ## Security model
54
+
55
+ - The key is public. It is accepted only from the allowlisted origins and only for subscription
56
+ registration. Never use a secret key (`papier_...`) in a browser; the SDK refuses it.
57
+ - Without a user token a device is anonymous. Papier will not let a page bind a device to a user ID
58
+ the server did not authorise.
59
+ - No cookies are sent and nothing is stored in `localStorage`; state is read from the browser.
60
+ - iOS and iPadOS only deliver Web Push to sites added to the Home Screen.
61
+
62
+ ## States and errors
63
+
64
+ `getState()` returns `unsupported`, `denied`, `prompt`, `unsubscribed` or `subscribed`. Failures are
65
+ `PapierBrowserError` with a `code`: `unsupported`, `permission-denied`, `invalid-key`,
66
+ `origin-not-allowed`, `invalid-user-token`, `rate-limited`, `service-unavailable`,
67
+ `service-worker`, `network`, `rejected`.
68
+
69
+ ## License
70
+
71
+ Use of this package is governed by the Papier SDK License in `LICENSE` and your Papier terms of service.
@@ -0,0 +1,55 @@
1
+ //#region src/push.d.ts
2
+ type PushState =
3
+ /** No service worker, Push API or Notification API. */
4
+ "unsupported" | "denied" |
5
+ /** Permission has not been requested yet. */
6
+ "prompt" | "unsubscribed" | "subscribed";
7
+ interface SubscribeOptions {
8
+ /**
9
+ * Binds this device to an end user. Mint it on your server with `issueUserToken`; pass the string,
10
+ * or a function that fetches it, so a stale token is never reused.
11
+ */
12
+ userToken?: string | (() => string | Promise<string>);
13
+ /** Where your site serves the Papier service worker. Defaults to `/papier-sw.js`. */
14
+ serviceWorkerPath?: string;
15
+ }
16
+ /** `bound` is true once the device is tied to an end user through a user token. */
17
+ interface RegisteredDevice {
18
+ id: string;
19
+ bound: boolean;
20
+ }
21
+ interface PapierPush {
22
+ isSupported(): boolean;
23
+ getState(): Promise<PushState>;
24
+ /** Asks for permission, subscribes and registers the device. Call it from a user gesture. */
25
+ subscribe(options?: SubscribeOptions): Promise<RegisteredDevice>;
26
+ /** Re-registers an existing subscription and resubscribes when Papier rotated its push key. */
27
+ refresh(options?: SubscribeOptions): Promise<PushState>;
28
+ unsubscribe(): Promise<void>;
29
+ }
30
+ //#endregion
31
+ //#region src/errors.d.ts
32
+ type PapierBrowserErrorCode = "unsupported" | "permission-denied" | "invalid-key" | "origin-not-allowed" | "invalid-user-token" | "rate-limited" | "service-unavailable" | "service-worker" | "network" | "rejected";
33
+ /** Every failure the SDK reports. `message` never contains keys, endpoints or tokens. */
34
+ export declare class PapierBrowserError extends Error {
35
+ readonly code: PapierBrowserErrorCode;
36
+ readonly status?: number | undefined;
37
+ readonly name = "PapierBrowserError";
38
+ constructor(code: PapierBrowserErrorCode, message: string, status?: number | undefined);
39
+ }
40
+ //#endregion
41
+ //#region src/index.d.ts
42
+ export interface PapierBrowserOptions {
43
+ /** A `papier_pk_...` key. Secret keys are rejected: they must never run in a browser. */
44
+ publishableKey: string;
45
+ /** Defaults to Papier's hosted API. */
46
+ apiUrl?: string;
47
+ fetch?: typeof fetch;
48
+ }
49
+ export interface PapierBrowser {
50
+ push: PapierPush;
51
+ }
52
+ export declare function createPapierBrowser(options: PapierBrowserOptions): PapierBrowser;
53
+ //#endregion
54
+ export type { PapierBrowserErrorCode, PapierPush, PushState, RegisteredDevice, SubscribeOptions };
55
+ //# sourceMappingURL=index.d.ts.map
package/build/index.js ADDED
@@ -0,0 +1,139 @@
1
+ //#region src/errors.ts
2
+ /** Every failure the SDK reports. `message` never contains keys, endpoints or tokens. */
3
+ var PapierBrowserError = class extends Error {
4
+ code;
5
+ status;
6
+ name = "PapierBrowserError";
7
+ constructor(code, message, status) {
8
+ super(message);
9
+ this.code = code;
10
+ this.status = status;
11
+ }
12
+ };
13
+
14
+ //#endregion
15
+ //#region src/vapid.ts
16
+ /** Web Push expects the VAPID public key as bytes, but the API serves it as base64url. */
17
+ function base64UrlToBytes(value) {
18
+ const padded = value.replace(/-/gu, "+").replace(/_/gu, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
19
+ const binary = atob(padded);
20
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
21
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
22
+ return bytes;
23
+ }
24
+ function bytesEqual(a, b) {
25
+ if (!a || a.byteLength !== b.byteLength) return false;
26
+ return new Uint8Array(a).every((byte, index) => byte === b[index]);
27
+ }
28
+
29
+ //#endregion
30
+ //#region src/push.ts
31
+ const DEFAULT_WORKER_PATH = "/papier-sw.js";
32
+ function createPush({ publishableKey, apiUrl, fetch: request }) {
33
+ const supported = () => typeof navigator !== "undefined" && "serviceWorker" in navigator && typeof PushManager !== "undefined" && typeof Notification !== "undefined";
34
+ async function call(path, init = {}) {
35
+ let response;
36
+ try {
37
+ response = await request(`${apiUrl}/api/public${path}`, {
38
+ method: init.method ?? "GET",
39
+ headers: {
40
+ authorization: `Bearer ${publishableKey}`,
41
+ ...init.body ? { "content-type": "application/json" } : {}
42
+ },
43
+ ...init.body ? { body: JSON.stringify(init.body) } : {},
44
+ credentials: "omit"
45
+ });
46
+ } catch {
47
+ throw new PapierBrowserError("network", "Could not reach Papier");
48
+ }
49
+ if (response.ok) return response;
50
+ const message = (await response.json().catch(() => ({}))).message ?? `Papier request failed (HTTP ${response.status})`;
51
+ if (response.status === 401) throw new PapierBrowserError("invalid-key", message, 401);
52
+ if (response.status === 403) throw new PapierBrowserError(/origin/iu.test(message) ? "origin-not-allowed" : "invalid-user-token", message, 403);
53
+ if (response.status === 429) throw new PapierBrowserError("rate-limited", message, 429);
54
+ if (response.status === 503) throw new PapierBrowserError("service-unavailable", message, 503);
55
+ throw new PapierBrowserError("rejected", message, response.status);
56
+ }
57
+ async function registration(path) {
58
+ try {
59
+ await navigator.serviceWorker.register(path);
60
+ return await navigator.serviceWorker.ready;
61
+ } catch {
62
+ throw new PapierBrowserError("service-worker", `Could not register the service worker at ${path}`);
63
+ }
64
+ }
65
+ /** The page's registration is found by scope, so it does not depend on the worker's file name. */
66
+ async function existing() {
67
+ return (await navigator.serviceWorker.getRegistration())?.pushManager.getSubscription() ?? null;
68
+ }
69
+ async function userToken(option) {
70
+ return typeof option === "function" ? option() : option;
71
+ }
72
+ async function register(subscription, options) {
73
+ const token = await userToken(options?.userToken);
74
+ return await (await call("/push/subscriptions", {
75
+ method: "POST",
76
+ body: {
77
+ subscription: subscription.toJSON(),
78
+ ...token ? { userToken: token } : {}
79
+ }
80
+ })).json();
81
+ }
82
+ async function subscription(registered) {
83
+ const key = base64UrlToBytes((await (await call("/push/config")).json()).vapidPublicKey);
84
+ const current = await registered.pushManager.getSubscription();
85
+ if (current && bytesEqual(current.options.applicationServerKey, key)) return current;
86
+ await current?.unsubscribe();
87
+ return registered.pushManager.subscribe({
88
+ userVisibleOnly: true,
89
+ applicationServerKey: key
90
+ });
91
+ }
92
+ return {
93
+ isSupported: supported,
94
+ async getState() {
95
+ if (!supported()) return "unsupported";
96
+ if (Notification.permission === "denied") return "denied";
97
+ if (Notification.permission === "default") return "prompt";
98
+ return await existing() ? "subscribed" : "unsubscribed";
99
+ },
100
+ async subscribe(options) {
101
+ if (!supported()) throw new PapierBrowserError("unsupported", "This browser does not support Web Push");
102
+ if ((Notification.permission === "default" ? await Notification.requestPermission() : Notification.permission) !== "granted") throw new PapierBrowserError("permission-denied", "Notification permission was not granted");
103
+ return register(await subscription(await registration(options?.serviceWorkerPath ?? DEFAULT_WORKER_PATH)), options);
104
+ },
105
+ async refresh(options) {
106
+ if (!supported()) return "unsupported";
107
+ if (Notification.permission !== "granted") return this.getState();
108
+ if (!await existing()) return "unsubscribed";
109
+ await register(await subscription(await registration(options?.serviceWorkerPath ?? DEFAULT_WORKER_PATH)), options);
110
+ return "subscribed";
111
+ },
112
+ async unsubscribe() {
113
+ if (!supported()) return;
114
+ const current = await existing();
115
+ if (!current) return;
116
+ await call("/push/subscriptions", {
117
+ method: "DELETE",
118
+ body: { endpoint: current.endpoint }
119
+ });
120
+ await current.unsubscribe();
121
+ }
122
+ };
123
+ }
124
+
125
+ //#endregion
126
+ //#region src/index.ts
127
+ function createPapierBrowser(options) {
128
+ const key = options.publishableKey;
129
+ if (!key.startsWith("papier_pk_")) throw new PapierBrowserError("invalid-key", key.startsWith("papier_") ? "Secret API keys must never be used in a browser. Use a publishable key (papier_pk_...)." : "Expected a publishable key starting with papier_pk_");
130
+ return { push: createPush({
131
+ publishableKey: key,
132
+ apiUrl: (options.apiUrl ?? "https://api.usepapier.io").replace(/\/+$/u, ""),
133
+ fetch: options.fetch ?? globalThis.fetch.bind(globalThis)
134
+ }) };
135
+ }
136
+
137
+ //#endregion
138
+ export { PapierBrowserError, createPapierBrowser };
139
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/vapid.ts","../src/push.ts","../src/index.ts"],"sourcesContent":["export type PapierBrowserErrorCode =\n | \"unsupported\"\n | \"permission-denied\"\n | \"invalid-key\"\n | \"origin-not-allowed\"\n | \"invalid-user-token\"\n | \"rate-limited\"\n | \"service-unavailable\"\n | \"service-worker\"\n | \"network\"\n | \"rejected\";\n\n/** Every failure the SDK reports. `message` never contains keys, endpoints or tokens. */\nexport class PapierBrowserError extends Error {\n readonly name = \"PapierBrowserError\";\n constructor(\n readonly code: PapierBrowserErrorCode,\n message: string,\n readonly status?: number,\n ) {\n super(message);\n }\n}\n","/** Web Push expects the VAPID public key as bytes, but the API serves it as base64url. */\nexport function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer> {\n const padded = value\n .replace(/-/gu, \"+\")\n .replace(/_/gu, \"/\")\n .padEnd(Math.ceil(value.length / 4) * 4, \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(new ArrayBuffer(binary.length));\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);\n return bytes;\n}\n\nexport function bytesEqual(a: ArrayBuffer | null | undefined, b: Uint8Array): boolean {\n if (!a || a.byteLength !== b.byteLength) return false;\n const view = new Uint8Array(a);\n return view.every((byte, index) => byte === b[index]);\n}\n","import { PapierBrowserError } from \"./errors\";\nimport { base64UrlToBytes, bytesEqual } from \"./vapid\";\n\nexport type PushState =\n /** No service worker, Push API or Notification API. */\n | \"unsupported\"\n | \"denied\"\n /** Permission has not been requested yet. */\n | \"prompt\"\n | \"unsubscribed\"\n | \"subscribed\";\n\nexport interface SubscribeOptions {\n /**\n * Binds this device to an end user. Mint it on your server with `issueUserToken`; pass the string,\n * or a function that fetches it, so a stale token is never reused.\n */\n userToken?: string | (() => string | Promise<string>);\n /** Where your site serves the Papier service worker. Defaults to `/papier-sw.js`. */\n serviceWorkerPath?: string;\n}\n\n/** `bound` is true once the device is tied to an end user through a user token. */\nexport interface RegisteredDevice {\n id: string;\n bound: boolean;\n}\n\nexport interface PapierPush {\n isSupported(): boolean;\n getState(): Promise<PushState>;\n /** Asks for permission, subscribes and registers the device. Call it from a user gesture. */\n subscribe(options?: SubscribeOptions): Promise<RegisteredDevice>;\n /** Re-registers an existing subscription and resubscribes when Papier rotated its push key. */\n refresh(options?: SubscribeOptions): Promise<PushState>;\n unsubscribe(): Promise<void>;\n}\n\nexport interface PushDependencies {\n publishableKey: string;\n apiUrl: string;\n fetch: typeof fetch;\n}\n\nconst DEFAULT_WORKER_PATH = \"/papier-sw.js\";\n\ninterface ApiFailure {\n code?: string;\n message?: string;\n}\n\nexport function createPush({\n publishableKey,\n apiUrl,\n fetch: request,\n}: PushDependencies): PapierPush {\n const supported = () =>\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n typeof PushManager !== \"undefined\" &&\n typeof Notification !== \"undefined\";\n\n async function call(path: string, init: { method?: string; body?: unknown } = {}) {\n let response: Response;\n try {\n response = await request(`${apiUrl}/api/public${path}`, {\n method: init.method ?? \"GET\",\n headers: {\n authorization: `Bearer ${publishableKey}`,\n ...(init.body ? { \"content-type\": \"application/json\" } : {}),\n },\n ...(init.body ? { body: JSON.stringify(init.body) } : {}),\n // The key is the credential: never send or accept cookies.\n credentials: \"omit\",\n });\n } catch {\n throw new PapierBrowserError(\"network\", \"Could not reach Papier\");\n }\n if (response.ok) return response;\n\n const failure = (await response.json().catch(() => ({}))) as ApiFailure;\n const message = failure.message ?? `Papier request failed (HTTP ${response.status})`;\n if (response.status === 401) throw new PapierBrowserError(\"invalid-key\", message, 401);\n if (response.status === 403) {\n throw new PapierBrowserError(\n /origin/iu.test(message) ? \"origin-not-allowed\" : \"invalid-user-token\",\n message,\n 403,\n );\n }\n if (response.status === 429) throw new PapierBrowserError(\"rate-limited\", message, 429);\n if (response.status === 503) throw new PapierBrowserError(\"service-unavailable\", message, 503);\n throw new PapierBrowserError(\"rejected\", message, response.status);\n }\n\n async function registration(path: string) {\n try {\n await navigator.serviceWorker.register(path);\n return await navigator.serviceWorker.ready;\n } catch {\n throw new PapierBrowserError(\n \"service-worker\",\n `Could not register the service worker at ${path}`,\n );\n }\n }\n\n /** The page's registration is found by scope, so it does not depend on the worker's file name. */\n async function existing() {\n const found = await navigator.serviceWorker.getRegistration();\n return found?.pushManager.getSubscription() ?? null;\n }\n\n async function userToken(option: SubscribeOptions[\"userToken\"]) {\n return typeof option === \"function\" ? option() : option;\n }\n\n async function register(\n subscription: PushSubscription,\n options: SubscribeOptions | undefined,\n ): Promise<RegisteredDevice> {\n const token = await userToken(options?.userToken);\n const response = await call(\"/push/subscriptions\", {\n method: \"POST\",\n body: {\n subscription: subscription.toJSON(),\n ...(token ? { userToken: token } : {}),\n },\n });\n return (await response.json()) as RegisteredDevice;\n }\n\n async function subscription(registered: ServiceWorkerRegistration) {\n const config = (await (await call(\"/push/config\")).json()) as { vapidPublicKey: string };\n const key = base64UrlToBytes(config.vapidPublicKey);\n const current = await registered.pushManager.getSubscription();\n if (current && bytesEqual(current.options.applicationServerKey, key)) return current;\n // A subscription made with a retired push key can never receive messages again.\n await current?.unsubscribe();\n return registered.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });\n }\n\n return {\n isSupported: supported,\n\n async getState() {\n if (!supported()) return \"unsupported\";\n if (Notification.permission === \"denied\") return \"denied\";\n if (Notification.permission === \"default\") return \"prompt\";\n return (await existing()) ? \"subscribed\" : \"unsubscribed\";\n },\n\n async subscribe(options) {\n if (!supported()) {\n throw new PapierBrowserError(\"unsupported\", \"This browser does not support Web Push\");\n }\n const permission =\n Notification.permission === \"default\"\n ? await Notification.requestPermission()\n : Notification.permission;\n if (permission !== \"granted\") {\n throw new PapierBrowserError(\n \"permission-denied\",\n \"Notification permission was not granted\",\n );\n }\n const registered = await registration(options?.serviceWorkerPath ?? DEFAULT_WORKER_PATH);\n return register(await subscription(registered), options);\n },\n\n async refresh(options) {\n if (!supported()) return \"unsupported\";\n if (Notification.permission !== \"granted\") return this.getState();\n if (!(await existing())) return \"unsubscribed\";\n const registered = await registration(options?.serviceWorkerPath ?? DEFAULT_WORKER_PATH);\n await register(await subscription(registered), options);\n return \"subscribed\";\n },\n\n async unsubscribe() {\n if (!supported()) return;\n const current = await existing();\n if (!current) return;\n // Tell Papier first: if the network fails the device must not be silently orphaned server-side.\n await call(\"/push/subscriptions\", { method: \"DELETE\", body: { endpoint: current.endpoint } });\n await current.unsubscribe();\n },\n };\n}\n","import { PapierBrowserError } from \"./errors\";\nimport { createPush, type PapierPush } from \"./push\";\n\nexport { PapierBrowserError, type PapierBrowserErrorCode } from \"./errors\";\nexport type { PapierPush, PushState, RegisteredDevice, SubscribeOptions } from \"./push\";\n\nexport interface PapierBrowserOptions {\n /** A `papier_pk_...` key. Secret keys are rejected: they must never run in a browser. */\n publishableKey: string;\n /** Defaults to Papier's hosted API. */\n apiUrl?: string;\n fetch?: typeof fetch;\n}\n\nexport interface PapierBrowser {\n push: PapierPush;\n}\n\nexport function createPapierBrowser(options: PapierBrowserOptions): PapierBrowser {\n const key = options.publishableKey;\n if (!key.startsWith(\"papier_pk_\")) {\n throw new PapierBrowserError(\n \"invalid-key\",\n key.startsWith(\"papier_\")\n ? \"Secret API keys must never be used in a browser. Use a publishable key (papier_pk_...).\"\n : \"Expected a publishable key starting with papier_pk_\",\n );\n }\n return {\n push: createPush({\n publishableKey: key,\n apiUrl: (options.apiUrl ?? \"https://api.usepapier.io\").replace(/\\/+$/u, \"\"),\n fetch: options.fetch ?? globalThis.fetch.bind(globalThis),\n }),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ (function(){function e(e){let t;try{t=e.data?.json()}catch{return}if(!t||typeof t!=`object`)return;let{title:n,body:r}=t;if(!(typeof n!=`string`||!n.trim()||typeof r!=`string`))return t}self.addEventListener(`push`,t=>{let n=e(t);if(!n)return;let r=n.data&&typeof n.data==`object`&&!Array.isArray(n.data)?n.data:{};t.waitUntil(self.registration.showNotification(n.title,{body:n.body,...typeof n.image==`string`&&n.image.startsWith(`https://`)?{image:n.image}:{},...typeof r.notificationId==`string`?{tag:r.notificationId}:{},data:{...r,url:typeof n.url==`string`?n.url:`/`}}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil((async()=>{let t;try{t=new URL(String(e.notification.data?.url??`/`),self.location.origin)}catch{return}if(t.origin!==self.location.origin||![`https:`,`http:`].includes(t.protocol))return;let n=await self.clients.matchAll({type:`window`,includeUncontrolled:!0});for(let e of n)if(new URL(e.url).origin===t.origin){let n=await e.navigate(t.href);if(n){await n.focus();return}}await self.clients.openWindow(t.href)})())})})();
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@usepapier/browser",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Papier browser SDK: Web Push subscriptions with a publishable key",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./build/index.d.ts",
10
+ "default": "./build/index.js"
11
+ },
12
+ "./service-worker": "./build/service-worker.iife.js"
13
+ },
14
+ "files": [
15
+ "build",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "pack": "NODE_ENV=production tsdown",
21
+ "tsc": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.worker.json",
22
+ "lint": "oxlint . --type-aware",
23
+ "lint:fix": "oxlint . --fix --type-aware",
24
+ "format": "oxfmt \"**/*.{mjs,ts,md,json}\" --check",
25
+ "format:fix": "oxfmt \"**/*.{mjs,ts,md,json}\" --write",
26
+ "test": "vitest run --config vitest.unit.config.ts",
27
+ "clean": "rimraf build tsconfig.tsbuildinfo"
28
+ },
29
+ "keywords": [
30
+ "papier",
31
+ "web-push",
32
+ "push-notifications",
33
+ "service-worker",
34
+ "browser",
35
+ "vapid",
36
+ "notifications",
37
+ "sdk",
38
+ "typescript"
39
+ ],
40
+ "license": "SEE LICENSE IN LICENSE",
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "devDependencies": {
45
+ "@papier/tsconfig": "workspace:*",
46
+ "@types/node": "^26.6.5",
47
+ "rimraf": "^6.1.3",
48
+ "tsdown": "^0.23.1",
49
+ "vitest": "^5.0.5"
50
+ },
51
+ "author": "Papier",
52
+ "homepage": "https://usepapier.io/docs/"
53
+ }