@sweidos/eidos 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/action.js +167 -94
- package/dist/cli.js +102 -0
- package/dist/devtools.js +80 -20
- package/dist/eidos-sw.js +280 -188
- package/dist/eidos.cjs +2 -2
- package/dist/index.d.ts +99 -4
- package/dist/index.js +42 -39
- package/dist/push.cjs +120 -0
- package/dist/push.d.ts +28 -0
- package/dist/push.js +113 -0
- package/dist/runtime.js +37 -20
- package/dist/sw-bridge.js +44 -31
- package/dist/version.js +1 -1
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -233,6 +233,81 @@ const mutation = useEidosMutation(createOrder, {
|
|
|
233
233
|
|
|
234
234
|
---
|
|
235
235
|
|
|
236
|
+
## Push Notifications
|
|
237
|
+
|
|
238
|
+
Headless, framework-agnostic Web Push. Tree-shaken via a separate subpath — adds zero bytes unless imported.
|
|
239
|
+
|
|
240
|
+
**1. Generate VAPID keys (one-time):**
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
npx @sweidos/eidos generate-vapid-keys
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Detects your framework (Vite/Next/SvelteKit/Nuxt) and writes a correctly-prefixed
|
|
247
|
+
public key + an unprefixed private key to `.env.local`:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
VITE_EIDOS_VAPID_PUBLIC_KEY=...
|
|
251
|
+
EIDOS_VAPID_PRIVATE_KEY=...
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Give `EIDOS_VAPID_PRIVATE_KEY` (and the public key) to your backend. What the
|
|
255
|
+
backend does with them — language, storage, send timing — is entirely its own
|
|
256
|
+
concern; Eidos never talks to it directly.
|
|
257
|
+
|
|
258
|
+
**2. Register handlers once at app init (any tab, no permission prompt):**
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { registerPushHandlers } from '@sweidos/eidos/push';
|
|
262
|
+
|
|
263
|
+
registerPushHandlers({
|
|
264
|
+
onNotificationClick: (data) => router.push(data.url),
|
|
265
|
+
onSubscriptionExpired: (sub) =>
|
|
266
|
+
fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**3. Subscribe from a user gesture (e.g. an "Enable notifications" button):**
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { subscribeToPush, isPushSupported, getPushPermissionState } from '@sweidos/eidos/push';
|
|
274
|
+
|
|
275
|
+
async function onEnableClick() {
|
|
276
|
+
const result = await subscribeToPush({
|
|
277
|
+
vapidPublicKey: import.meta.env.VITE_EIDOS_VAPID_PUBLIC_KEY,
|
|
278
|
+
onSubscribe: (sub) =>
|
|
279
|
+
fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (result.status === 'subscribed') toast('Notifications enabled');
|
|
283
|
+
else if (result.status === 'denied') toast('Permission denied');
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`isPushSupported()` / `getPushPermissionState()` / `getPushUnsupportedReason()`
|
|
288
|
+
let you hide the button when push is unavailable (e.g. iOS Safari outside an
|
|
289
|
+
installed PWA returns `'ios-not-installed'`).
|
|
290
|
+
|
|
291
|
+
### Server payload schema
|
|
292
|
+
|
|
293
|
+
The service worker shows whatever your server sends — Eidos never renders UI:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"title": "Order shipped",
|
|
298
|
+
"body": "Your order #1234 is on its way",
|
|
299
|
+
"icon": "/icon.png",
|
|
300
|
+
"badge": "/badge.png",
|
|
301
|
+
"tag": "order-1234",
|
|
302
|
+
"data": { "url": "/orders/1234" }
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Click behavior: if the app is open, `data` is delivered to `onNotificationClick`
|
|
307
|
+
for client-side routing; otherwise the SW opens `data.url` directly.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
236
311
|
## Testing
|
|
237
312
|
|
|
238
313
|
```ts
|
package/dist/action.js
CHANGED
|
@@ -1,66 +1,99 @@
|
|
|
1
|
-
import { useEidosStore as
|
|
2
|
-
import { getSwRegistration as
|
|
3
|
-
import { idbQueueStorage as
|
|
4
|
-
import { _getQueueStorage as
|
|
5
|
-
var
|
|
1
|
+
import { useEidosStore as y } from "./store.js";
|
|
2
|
+
import { getSwRegistration as _ } from "./sw-bridge.js";
|
|
3
|
+
import { idbQueueStorage as S } from "./idb.js";
|
|
4
|
+
import { _getQueueStorage as M } from "./queue-storage.js";
|
|
5
|
+
var h = /* @__PURE__ */ new Map(), k = /* @__PURE__ */ new Map(), C = /* @__PURE__ */ new Map(), Q = /* @__PURE__ */ new Map(), x = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map();
|
|
6
6
|
function u() {
|
|
7
|
-
return
|
|
7
|
+
return M() ?? S;
|
|
8
8
|
}
|
|
9
|
-
function
|
|
9
|
+
function m() {
|
|
10
10
|
return crypto.randomUUID();
|
|
11
11
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
function v(e, t, i) {
|
|
13
|
+
return e(...t, i);
|
|
14
|
+
}
|
|
15
|
+
function U(e, t) {
|
|
16
|
+
const i = t.name || e.name || m(), a = t.namespace ? `${t.namespace}::${i}` : i;
|
|
17
|
+
h.set(a, e), x.set(a, t), t.onRollback && k.set(a, t.onRollback), t.onConflict && C.set(a, t.onConflict), t.conflict && Q.set(a, t.conflict);
|
|
18
|
+
const c = async (...n) => {
|
|
19
|
+
const { isOnline: s } = y.getState(), l = t.reliability === "neverLose" || t.cancellable, o = l ? m() : "";
|
|
20
|
+
let d;
|
|
21
|
+
if (t.cancellable) {
|
|
22
|
+
const f = new AbortController();
|
|
23
|
+
p.set(o, f), d = f.signal;
|
|
24
24
|
}
|
|
25
|
+
const g = {
|
|
26
|
+
idempotencyKey: o,
|
|
27
|
+
attempt: 0,
|
|
28
|
+
signal: d
|
|
29
|
+
};
|
|
30
|
+
t.onOptimistic?.(...n, g);
|
|
25
31
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
if (t.reliability === "neverLose") {
|
|
33
|
+
if (!s) return R(a, a, n, t, o);
|
|
34
|
+
try {
|
|
35
|
+
return await v(e, n, g);
|
|
36
|
+
} catch (f) {
|
|
37
|
+
if (A(f)) throw f;
|
|
38
|
+
return R(a, a, n, t, o);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return l ? await v(e, n, g) : await e(...n);
|
|
43
|
+
} catch (f) {
|
|
44
|
+
throw t.onRollback?.(...n), f;
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
t.cancellable && p.delete(o);
|
|
29
48
|
}
|
|
49
|
+
}, r = async (n) => {
|
|
50
|
+
const s = p.get(n);
|
|
51
|
+
if (s)
|
|
52
|
+
return s.abort(), !0;
|
|
53
|
+
const l = (await u().getAll()).find((o) => o.idempotencyKey === n && o.status === "pending");
|
|
54
|
+
return l ? (y.getState().removeQueueItem(l.id), await u().remove(l.id), !0) : !1;
|
|
30
55
|
};
|
|
31
|
-
return Object.defineProperty(
|
|
32
|
-
value:
|
|
56
|
+
return Object.defineProperty(c, "id", {
|
|
57
|
+
value: a,
|
|
33
58
|
writable: !1
|
|
34
|
-
}), Object.defineProperty(
|
|
59
|
+
}), Object.defineProperty(c, "config", {
|
|
35
60
|
value: t,
|
|
36
61
|
writable: !1
|
|
37
|
-
}),
|
|
62
|
+
}), Object.defineProperty(c, "cancel", {
|
|
63
|
+
value: r,
|
|
64
|
+
writable: !1
|
|
65
|
+
}), c;
|
|
38
66
|
}
|
|
39
|
-
async function
|
|
40
|
-
const
|
|
41
|
-
|
|
67
|
+
async function R(e, t, i, a, c) {
|
|
68
|
+
const r = m(), n = {
|
|
69
|
+
schemaVersion: 2,
|
|
70
|
+
id: r,
|
|
42
71
|
actionId: e,
|
|
43
72
|
actionName: t,
|
|
44
|
-
|
|
73
|
+
idempotencyKey: c,
|
|
74
|
+
args: i,
|
|
45
75
|
queuedAt: Date.now(),
|
|
46
76
|
retryCount: 0,
|
|
47
77
|
maxRetries: a.maxRetries ?? 3,
|
|
48
78
|
status: "pending",
|
|
49
79
|
priority: a.priority ?? "normal"
|
|
50
80
|
};
|
|
51
|
-
await u().add(n),
|
|
81
|
+
await u().add(n), y.getState().addQueueItem(n);
|
|
52
82
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
83
|
+
const s = _();
|
|
84
|
+
s && "sync" in s && await s.sync.register("eidos-queue-replay");
|
|
55
85
|
} catch {
|
|
56
86
|
}
|
|
57
87
|
return {
|
|
58
88
|
queued: !0,
|
|
59
|
-
id:
|
|
89
|
+
id: r,
|
|
60
90
|
message: `"${t}" queued — will execute when online`
|
|
61
91
|
};
|
|
62
92
|
}
|
|
63
|
-
function
|
|
93
|
+
function A(e) {
|
|
94
|
+
return e instanceof DOMException && e.name === "AbortError";
|
|
95
|
+
}
|
|
96
|
+
function K(e) {
|
|
64
97
|
if (e instanceof Response) return e.status >= 400 && e.status < 500;
|
|
65
98
|
if (typeof e == "object" && e !== null) {
|
|
66
99
|
const t = e.status;
|
|
@@ -68,112 +101,152 @@ function h(e) {
|
|
|
68
101
|
}
|
|
69
102
|
return !1;
|
|
70
103
|
}
|
|
71
|
-
function
|
|
104
|
+
function O(e) {
|
|
72
105
|
return Math.min(2e3 * 2 ** e, 3e5) * (0.8 + Math.random() * 0.4);
|
|
73
106
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const e = s.getState();
|
|
77
|
-
if (!e.isOnline || c) return {
|
|
107
|
+
function w() {
|
|
108
|
+
return {
|
|
78
109
|
attempted: 0,
|
|
79
110
|
succeeded: 0,
|
|
80
111
|
failed: 0,
|
|
81
112
|
retrying: 0,
|
|
82
113
|
skipped: 0,
|
|
83
|
-
conflicted: 0
|
|
114
|
+
conflicted: 0,
|
|
115
|
+
cancelled: 0
|
|
84
116
|
};
|
|
85
|
-
|
|
117
|
+
}
|
|
118
|
+
var b = !1, q = "eidos-queue-replay";
|
|
119
|
+
async function $() {
|
|
120
|
+
const e = y.getState();
|
|
121
|
+
if (!e.isOnline) return w();
|
|
122
|
+
if (typeof navigator < "u" && navigator.locks) return navigator.locks.request(q, { ifAvailable: !0 }, async (t) => t ? I(e) : w());
|
|
123
|
+
if (b) return w();
|
|
124
|
+
b = !0;
|
|
86
125
|
try {
|
|
87
|
-
return await
|
|
126
|
+
return await I(e);
|
|
88
127
|
} finally {
|
|
89
|
-
|
|
128
|
+
b = !1;
|
|
90
129
|
}
|
|
91
130
|
}
|
|
92
|
-
async function
|
|
93
|
-
const
|
|
94
|
-
if (!
|
|
131
|
+
async function E(e, t) {
|
|
132
|
+
const i = h.get(e.actionId);
|
|
133
|
+
if (!i) return "skipped";
|
|
134
|
+
const a = x.get(e.actionId)?.cancellable;
|
|
135
|
+
let c;
|
|
136
|
+
if (a) {
|
|
137
|
+
const n = new AbortController();
|
|
138
|
+
p.set(e.idempotencyKey, n), c = n.signal;
|
|
139
|
+
}
|
|
140
|
+
const r = {
|
|
141
|
+
idempotencyKey: e.idempotencyKey,
|
|
142
|
+
attempt: e.retryCount,
|
|
143
|
+
signal: c
|
|
144
|
+
};
|
|
95
145
|
try {
|
|
96
|
-
await
|
|
97
|
-
const
|
|
146
|
+
await v(i, e.args, r);
|
|
147
|
+
const n = Date.now();
|
|
98
148
|
return t.updateQueueItem(e.id, {
|
|
99
149
|
status: "succeeded",
|
|
100
|
-
completedAt:
|
|
150
|
+
completedAt: n
|
|
101
151
|
}), await u().update(e.id, {
|
|
102
152
|
status: "succeeded",
|
|
103
|
-
completedAt:
|
|
153
|
+
completedAt: n
|
|
104
154
|
}), setTimeout(() => {
|
|
105
155
|
t.removeQueueItem(e.id), u().remove(e.id);
|
|
106
156
|
}, 3e3), "succeeded";
|
|
107
|
-
} catch (
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
157
|
+
} catch (n) {
|
|
158
|
+
if (A(n))
|
|
159
|
+
return t.removeQueueItem(e.id), await u().remove(e.id), "cancelled";
|
|
160
|
+
if (K(n)) {
|
|
161
|
+
const l = Q.get(e.actionId);
|
|
162
|
+
let o;
|
|
163
|
+
if (l) switch (l.strategy) {
|
|
164
|
+
case "serverWins":
|
|
165
|
+
o = "skip";
|
|
166
|
+
break;
|
|
167
|
+
case "clientWins":
|
|
168
|
+
case "lastWriteWins":
|
|
169
|
+
o = "retry";
|
|
170
|
+
break;
|
|
171
|
+
case "merge":
|
|
172
|
+
case "custom": {
|
|
173
|
+
const d = {
|
|
174
|
+
error: n,
|
|
175
|
+
args: e.args,
|
|
176
|
+
attempt: e.retryCount,
|
|
177
|
+
idempotencyKey: e.idempotencyKey
|
|
178
|
+
};
|
|
179
|
+
o = l.resolve?.(d) ?? "retry";
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const d = C.get(e.actionId);
|
|
185
|
+
d && (o = d(n, e.args));
|
|
186
|
+
}
|
|
187
|
+
if (o === "skip")
|
|
111
188
|
return t.removeQueueItem(e.id), await u().remove(e.id), "conflicted";
|
|
189
|
+
o && typeof o == "object" && (e.args = o.resolved, t.updateQueueItem(e.id, { args: o.resolved }), await u().update(e.id, { args: o.resolved }));
|
|
112
190
|
}
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
191
|
+
const s = e.retryCount + 1;
|
|
192
|
+
if (s >= e.maxRetries)
|
|
115
193
|
return t.updateQueueItem(e.id, {
|
|
116
194
|
status: "failed",
|
|
117
|
-
error: String(
|
|
118
|
-
retryCount:
|
|
195
|
+
error: String(n),
|
|
196
|
+
retryCount: s
|
|
119
197
|
}), await u().update(e.id, {
|
|
120
198
|
status: "failed",
|
|
121
|
-
error: String(
|
|
122
|
-
retryCount:
|
|
123
|
-
}),
|
|
199
|
+
error: String(n),
|
|
200
|
+
retryCount: s
|
|
201
|
+
}), k.get(e.actionId)?.(...e.args), "failed";
|
|
124
202
|
{
|
|
125
|
-
const
|
|
203
|
+
const l = Date.now() + O(s);
|
|
126
204
|
return t.updateQueueItem(e.id, {
|
|
127
205
|
status: "pending",
|
|
128
|
-
retryCount:
|
|
129
|
-
nextRetryAt:
|
|
206
|
+
retryCount: s,
|
|
207
|
+
nextRetryAt: l
|
|
130
208
|
}), await u().update(e.id, {
|
|
131
209
|
status: "pending",
|
|
132
|
-
retryCount:
|
|
133
|
-
nextRetryAt:
|
|
210
|
+
retryCount: s,
|
|
211
|
+
nextRetryAt: l
|
|
134
212
|
}), "retrying";
|
|
135
213
|
}
|
|
214
|
+
} finally {
|
|
215
|
+
a && p.delete(e.idempotencyKey);
|
|
136
216
|
}
|
|
137
217
|
}
|
|
138
|
-
async function
|
|
218
|
+
async function D(e, t, i) {
|
|
139
219
|
if (e.length === 0) return;
|
|
140
|
-
const a = e.filter((
|
|
141
|
-
if (
|
|
142
|
-
t.batchUpdateQueueItems(a.map((
|
|
143
|
-
id:
|
|
220
|
+
const a = e.filter((r) => h.has(r.actionId));
|
|
221
|
+
if (i.skipped += e.length - a.length, a.length > 0) {
|
|
222
|
+
t.batchUpdateQueueItems(a.map((r) => ({
|
|
223
|
+
id: r.id,
|
|
144
224
|
update: { status: "replaying" }
|
|
145
225
|
})));
|
|
146
|
-
for (const
|
|
226
|
+
for (const r of a) u().update(r.id, { status: "replaying" });
|
|
147
227
|
}
|
|
148
|
-
const
|
|
149
|
-
for (const
|
|
150
|
-
const
|
|
151
|
-
|
|
228
|
+
const c = await Promise.allSettled(a.map((r) => E(r, t)));
|
|
229
|
+
for (const r of c) {
|
|
230
|
+
const n = r.status === "fulfilled" ? r.value : "failed";
|
|
231
|
+
n === "skipped" ? i.skipped++ : n === "conflicted" ? i.conflicted++ : n === "cancelled" ? i.cancelled++ : (i.attempted++, i[n]++);
|
|
152
232
|
}
|
|
153
233
|
}
|
|
154
|
-
async function
|
|
155
|
-
const t = await u().getPending(),
|
|
156
|
-
|
|
157
|
-
succeeded: 0,
|
|
158
|
-
failed: 0,
|
|
159
|
-
retrying: 0,
|
|
160
|
-
skipped: 0,
|
|
161
|
-
conflicted: 0
|
|
162
|
-
};
|
|
163
|
-
for (const n of [
|
|
234
|
+
async function I(e) {
|
|
235
|
+
const t = await u().getPending(), i = Date.now(), a = t.filter((r) => r.retryCount < r.maxRetries && (!r.nextRetryAt || r.nextRetryAt <= i)), c = w();
|
|
236
|
+
for (const r of [
|
|
164
237
|
"high",
|
|
165
238
|
"normal",
|
|
166
239
|
"low"
|
|
167
|
-
]) await
|
|
168
|
-
return
|
|
240
|
+
]) await D(a.filter((n) => (n.priority ?? "normal") === r), e, c);
|
|
241
|
+
return c;
|
|
169
242
|
}
|
|
170
|
-
async function
|
|
171
|
-
await u().clear(),
|
|
243
|
+
async function T() {
|
|
244
|
+
await u().clear(), y.getState().hydrateQueue([]);
|
|
172
245
|
}
|
|
173
246
|
export {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
247
|
+
U as action,
|
|
248
|
+
T as clearQueue,
|
|
249
|
+
$ as replayQueue
|
|
177
250
|
};
|
|
178
251
|
|
|
179
252
|
//# sourceMappingURL=action.js.map
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
3
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
//#region src/cli.ts
|
|
7
|
+
var PUBLIC_KEY_NAME = "EIDOS_VAPID_PUBLIC_KEY";
|
|
8
|
+
var PRIVATE_KEY_NAME = "EIDOS_VAPID_PRIVATE_KEY";
|
|
9
|
+
function base64UrlFromBuffer(buf) {
|
|
10
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
11
|
+
}
|
|
12
|
+
function base64UrlToBuffer(b64url) {
|
|
13
|
+
const b64 = (b64url + "=".repeat((4 - b64url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
14
|
+
return Buffer.from(b64, "base64");
|
|
15
|
+
}
|
|
16
|
+
/** Pads a base64url-encoded big-endian integer to `length` bytes (leading zeros). */
|
|
17
|
+
function padTo(b64url, length) {
|
|
18
|
+
const buf = base64UrlToBuffer(b64url);
|
|
19
|
+
if (buf.length === length) return buf;
|
|
20
|
+
const padded = Buffer.alloc(length);
|
|
21
|
+
buf.copy(padded, length - buf.length);
|
|
22
|
+
return padded;
|
|
23
|
+
}
|
|
24
|
+
function generateVapidKeys() {
|
|
25
|
+
const { publicKey, privateKey } = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
|
|
26
|
+
const pubJwk = publicKey.export({ format: "jwk" });
|
|
27
|
+
const privJwk = privateKey.export({ format: "jwk" });
|
|
28
|
+
const x = padTo(pubJwk.x, 32);
|
|
29
|
+
const y = padTo(pubJwk.y, 32);
|
|
30
|
+
const point = Buffer.concat([
|
|
31
|
+
Buffer.from([4]),
|
|
32
|
+
x,
|
|
33
|
+
y
|
|
34
|
+
]);
|
|
35
|
+
const d = padTo(privJwk.d, 32);
|
|
36
|
+
return {
|
|
37
|
+
publicKey: base64UrlFromBuffer(point),
|
|
38
|
+
privateKey: base64UrlFromBuffer(d)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function detectEnvPrefix(cwd) {
|
|
42
|
+
if (existsSync(resolve(cwd, "next.config.js")) || existsSync(resolve(cwd, "next.config.ts"))) return "NEXT_PUBLIC_";
|
|
43
|
+
if (existsSync(resolve(cwd, "vite.config.ts")) || existsSync(resolve(cwd, "vite.config.js"))) return "VITE_";
|
|
44
|
+
if (existsSync(resolve(cwd, "svelte.config.js"))) return "PUBLIC_";
|
|
45
|
+
if (existsSync(resolve(cwd, "nuxt.config.ts")) || existsSync(resolve(cwd, "nuxt.config.js"))) return "NUXT_PUBLIC_";
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
function pickEnvFile(cwd) {
|
|
49
|
+
if (existsSync(resolve(cwd, ".env.local"))) return resolve(cwd, ".env.local");
|
|
50
|
+
if (existsSync(resolve(cwd, ".env"))) return resolve(cwd, ".env");
|
|
51
|
+
return resolve(cwd, ".env.local");
|
|
52
|
+
}
|
|
53
|
+
async function confirm(message) {
|
|
54
|
+
const rl = createInterface({
|
|
55
|
+
input: process.stdin,
|
|
56
|
+
output: process.stdout
|
|
57
|
+
});
|
|
58
|
+
const answer = await rl.question(`${message} (type "yes" to continue): `);
|
|
59
|
+
rl.close();
|
|
60
|
+
return answer.trim().toLowerCase() === "yes";
|
|
61
|
+
}
|
|
62
|
+
async function generateVapidKeysCommand() {
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
const publicKeyName = `${detectEnvPrefix(cwd)}${PUBLIC_KEY_NAME}`;
|
|
65
|
+
const force = process.argv.includes("--force");
|
|
66
|
+
const envFile = pickEnvFile(cwd);
|
|
67
|
+
const existing = existsSync(envFile) ? readFileSync(envFile, "utf8") : "";
|
|
68
|
+
const hasPublic = new RegExp(`^${publicKeyName}=`, "m").test(existing);
|
|
69
|
+
const hasPrivate = new RegExp(`^${PRIVATE_KEY_NAME}=`, "m").test(existing);
|
|
70
|
+
if (hasPublic && hasPrivate) {
|
|
71
|
+
if (!force) {
|
|
72
|
+
console.log(`VAPID keys already configured in ${envFile} — nothing to do.`);
|
|
73
|
+
console.log("Pass --force to regenerate (this invalidates ALL existing push subscriptions).");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!await confirm(`⚠ Regenerating VAPID keys will invalidate ALL existing push subscriptions in ${envFile}. Continue?`)) {
|
|
77
|
+
console.log("Aborted.");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const { publicKey, privateKey } = generateVapidKeys();
|
|
82
|
+
const lines = [`${publicKeyName}=${publicKey}`, `${PRIVATE_KEY_NAME}=${privateKey}`];
|
|
83
|
+
if (hasPublic && hasPrivate) writeFileSync(envFile, `${existing.split("\n").filter((line) => !line.startsWith(`${publicKeyName}=`) && !line.startsWith(`${PRIVATE_KEY_NAME}=`)).join("\n").replace(/\n+$/, "")}\n${lines.join("\n")}\n`);
|
|
84
|
+
else appendFileSync(envFile, `${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${lines.join("\n")}\n`);
|
|
85
|
+
console.log(`✓ VAPID keys written to ${envFile}`);
|
|
86
|
+
console.log("");
|
|
87
|
+
console.log(` ${publicKeyName}=${publicKey}`);
|
|
88
|
+
console.log(` ${PRIVATE_KEY_NAME}=${privateKey}`);
|
|
89
|
+
console.log("");
|
|
90
|
+
console.log(`Give ${PRIVATE_KEY_NAME} and ${publicKeyName} to your backend.`);
|
|
91
|
+
console.log("Backend needs a VAPID-capable web-push library (any language) to send notifications using subscription objects received via onSubscribe.");
|
|
92
|
+
}
|
|
93
|
+
var command = process.argv[2];
|
|
94
|
+
switch (command) {
|
|
95
|
+
case "generate-vapid-keys":
|
|
96
|
+
await generateVapidKeysCommand();
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
console.log("Usage: eidos generate-vapid-keys [--force]");
|
|
100
|
+
process.exit(command ? 1 : 0);
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|