opensentinel 2.1.1 → 3.1.1
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 +354 -283
- package/dist/archiver-AVNBYCKQ.js +15340 -0
- package/dist/archiver-AVNBYCKQ.js.map +1 -0
- package/dist/audit-logger-OBPR7CRO.js +22 -0
- package/dist/auth-UOX5K2BE.js +18 -0
- package/dist/autonomy-ZXDBDQUJ.js +86 -0
- package/dist/autonomy-ZXDBDQUJ.js.map +1 -0
- package/dist/aws-s3-Q4LLZZPD.js +146 -0
- package/dist/aws-s3-Q4LLZZPD.js.map +1 -0
- package/dist/backup-restore-PZ7CYYB7.js +16 -0
- package/dist/blocks-R3PODY47.js +23 -0
- package/dist/bot-QRARP4UN.js +36 -0
- package/dist/brain-7XLLM3KC.js +56 -0
- package/dist/camera-monitor-M5CYKUU4.js +335 -0
- package/dist/camera-monitor-M5CYKUU4.js.map +1 -0
- package/dist/{charts-MMXM6BWW.js → charts-V7ARZNKF.js} +2 -2
- package/dist/chunk-22VGGA7S.js +330 -0
- package/dist/chunk-22VGGA7S.js.map +1 -0
- package/dist/chunk-35WYTA3C.js +382 -0
- package/dist/chunk-35WYTA3C.js.map +1 -0
- package/dist/chunk-3E2PSU2C.js +146 -0
- package/dist/chunk-3E2PSU2C.js.map +1 -0
- package/dist/{chunk-L3F43VPB.js → chunk-4GLYY4NN.js} +2 -2
- package/dist/{chunk-L3F43VPB.js.map → chunk-4GLYY4NN.js.map} +1 -1
- package/dist/{chunk-L3PDU3XN.js → chunk-4UOE5TUZ.js} +4 -4
- package/dist/{chunk-6SNHU3CY.js → chunk-66OJ3WB4.js} +2 -2
- package/dist/chunk-6KONMXQ6.js +297 -0
- package/dist/chunk-6KONMXQ6.js.map +1 -0
- package/dist/chunk-6PMVAAA7.js +196 -0
- package/dist/chunk-6PMVAAA7.js.map +1 -0
- package/dist/chunk-766ASQWE.js +32620 -0
- package/dist/chunk-766ASQWE.js.map +1 -0
- package/dist/chunk-7WQO5J2M.js +29 -0
- package/dist/chunk-7WQO5J2M.js.map +1 -0
- package/dist/chunk-APHSRMBS.js +148 -0
- package/dist/chunk-APHSRMBS.js.map +1 -0
- package/dist/{chunk-4LVWXUNC.js → chunk-AYUKPTSM.js} +57 -39
- package/dist/chunk-AYUKPTSM.js.map +1 -0
- package/dist/chunk-BIPYADGB.js +84 -0
- package/dist/chunk-BIPYADGB.js.map +1 -0
- package/dist/chunk-BRBWNV65.js +457 -0
- package/dist/chunk-BRBWNV65.js.map +1 -0
- package/dist/chunk-BXZ6EA52.js +382 -0
- package/dist/chunk-BXZ6EA52.js.map +1 -0
- package/dist/chunk-EVE7MIIY.js +290 -0
- package/dist/chunk-EVE7MIIY.js.map +1 -0
- package/dist/chunk-F3TTNID2.js +138 -0
- package/dist/chunk-F3TTNID2.js.map +1 -0
- package/dist/chunk-H5RQOFO2.js +190 -0
- package/dist/chunk-H5RQOFO2.js.map +1 -0
- package/dist/chunk-HN3F4WSW.js +145 -0
- package/dist/chunk-HN3F4WSW.js.map +1 -0
- package/dist/{chunk-6DRDKB45.js → chunk-I6BDYQIG.js} +20 -9
- package/dist/chunk-I6BDYQIG.js.map +1 -0
- package/dist/chunk-IZJMVV7O.js +347 -0
- package/dist/chunk-IZJMVV7O.js.map +1 -0
- package/dist/chunk-KM22GV7G.js +211 -0
- package/dist/chunk-KM22GV7G.js.map +1 -0
- package/dist/chunk-MGFBLVR7.js +103 -0
- package/dist/chunk-MGFBLVR7.js.map +1 -0
- package/dist/chunk-MQJ2ECQT.js +228 -0
- package/dist/chunk-MQJ2ECQT.js.map +1 -0
- package/dist/{chunk-F6QUZQGI.js → chunk-MXAPLSJ5.js} +2 -2
- package/dist/{chunk-GK3E2I7A.js → chunk-NHMBTUMW.js} +2 -2
- package/dist/chunk-NPRTSZIF.js +131 -0
- package/dist/chunk-NPRTSZIF.js.map +1 -0
- package/dist/chunk-O7IH7JTI.js +1898 -0
- package/dist/chunk-O7IH7JTI.js.map +1 -0
- package/dist/chunk-OCVQGBJK.js +293 -0
- package/dist/chunk-OCVQGBJK.js.map +1 -0
- package/dist/chunk-P6QINGFL.js +332 -0
- package/dist/chunk-P6QINGFL.js.map +1 -0
- package/dist/chunk-PHDZKPNE.js +91 -0
- package/dist/chunk-PHDZKPNE.js.map +1 -0
- package/dist/chunk-PLDDJCW6.js +49 -0
- package/dist/chunk-PTGTGXV2.js +164 -0
- package/dist/chunk-PTGTGXV2.js.map +1 -0
- package/dist/chunk-REMIY4U2.js +171 -0
- package/dist/chunk-REMIY4U2.js.map +1 -0
- package/dist/chunk-RZ4YESBG.js +141 -0
- package/dist/chunk-RZ4YESBG.js.map +1 -0
- package/dist/chunk-SAX5MHK4.js +111 -0
- package/dist/chunk-SAX5MHK4.js.map +1 -0
- package/dist/{chunk-GVJVEWHI.js → chunk-SJSUSJ47.js} +2 -2
- package/dist/chunk-SPPMCAKG.js +777 -0
- package/dist/chunk-SPPMCAKG.js.map +1 -0
- package/dist/chunk-SVAPX2XN.js +2441 -0
- package/dist/chunk-SVAPX2XN.js.map +1 -0
- package/dist/chunk-TVEWKIK3.js +452 -0
- package/dist/chunk-TVEWKIK3.js.map +1 -0
- package/dist/{chunk-HH2HBTQM.js → chunk-TYAGMJNV.js} +5 -5
- package/dist/{chunk-JXUP2X7V.js → chunk-VEHFVBLI.js} +2 -2
- package/dist/chunk-VNX5GMTN.js +128 -0
- package/dist/chunk-VNX5GMTN.js.map +1 -0
- package/dist/chunk-VRD5CYRL.js +1568 -0
- package/dist/chunk-VRD5CYRL.js.map +1 -0
- package/dist/chunk-WLUHNG6X.js +122 -0
- package/dist/chunk-WLUHNG6X.js.map +1 -0
- package/dist/chunk-WRAKK6K6.js +265 -0
- package/dist/chunk-WRAKK6K6.js.map +1 -0
- package/dist/chunk-XKYRH4FM.js +681 -0
- package/dist/chunk-XKYRH4FM.js.map +1 -0
- package/dist/{chunk-GUBEEYDW.js → chunk-XMCVRVTF.js} +2 -2
- package/dist/{chunk-GUBEEYDW.js.map → chunk-XMCVRVTF.js.map} +1 -1
- package/dist/chunk-ZLZKF2PM.js +310 -0
- package/dist/chunk-ZLZKF2PM.js.map +1 -0
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/client-ZQSFPMOB.js +21 -0
- package/dist/clipboard-manager-TEO2GEDN.js +24 -0
- package/dist/commands/setup.js +3 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/start.js +3 -3
- package/dist/commands/status.js +2 -2
- package/dist/commands/stop.js +2 -2
- package/dist/commands/utils.js +2 -2
- package/dist/cron-explain-HHQKPD3M.js +16 -0
- package/dist/crypto-4AP47IKC.js +14 -0
- package/dist/crypto-4AP47IKC.js.map +1 -0
- package/dist/databases-37X4CI2Y.js +21 -0
- package/dist/databases-37X4CI2Y.js.map +1 -0
- package/dist/discord-B3HUPGQ6.js +70 -0
- package/dist/discord-B3HUPGQ6.js.map +1 -0
- package/dist/dist-UISMLMFN.js +21847 -0
- package/dist/dist-UISMLMFN.js.map +1 -0
- package/dist/email-K7LO2IPB.js +268 -0
- package/dist/email-K7LO2IPB.js.map +1 -0
- package/dist/enhanced-retrieval-DNLLEM4Z.js +753 -0
- package/dist/enhanced-retrieval-DNLLEM4Z.js.map +1 -0
- package/dist/enrichment-pipeline-MNHNW65K.js +13 -0
- package/dist/enrichment-pipeline-MNHNW65K.js.map +1 -0
- package/dist/entity-resolution-Y3IUWEAT.js +24 -0
- package/dist/entity-resolution-Y3IUWEAT.js.map +1 -0
- package/dist/env-IWXUVTCB.js +12 -0
- package/dist/env-IWXUVTCB.js.map +1 -0
- package/dist/google-workspace-DKWUVNGC.js +169 -0
- package/dist/google-workspace-DKWUVNGC.js.map +1 -0
- package/dist/hash-tool-ULQYD7B5.js +22 -0
- package/dist/hash-tool-ULQYD7B5.js.map +1 -0
- package/dist/heartbeat-monitor-GCISLXI3.js +22 -0
- package/dist/heartbeat-monitor-GCISLXI3.js.map +1 -0
- package/dist/image-generation-OSU7FP6F.js +486 -0
- package/dist/image-generation-OSU7FP6F.js.map +1 -0
- package/dist/imessage-NGA2XF2V.js +35 -0
- package/dist/imessage-NGA2XF2V.js.map +1 -0
- package/dist/inbox-summarizer-NRI4S7IF.js +47 -0
- package/dist/inbox-summarizer-NRI4S7IF.js.map +1 -0
- package/dist/incident-response-C5J7Q6DT.js +244 -0
- package/dist/incident-response-C5J7Q6DT.js.map +1 -0
- package/dist/inventory-manager-352OHXWD.js +24 -0
- package/dist/inventory-manager-352OHXWD.js.map +1 -0
- package/dist/jira-GSGDBMIG.js +199 -0
- package/dist/jira-GSGDBMIG.js.map +1 -0
- package/dist/json-tool-QE2SYHEG.js +26 -0
- package/dist/json-tool-QE2SYHEG.js.map +1 -0
- package/dist/key-rotation-DPHU4ZTB.js +18 -0
- package/dist/key-rotation-DPHU4ZTB.js.map +1 -0
- package/dist/lib.d.ts +603 -11
- package/dist/lib.js +161 -35
- package/dist/lib.js.map +1 -1
- package/dist/mailchimp-KKNF6QJ7.js +152 -0
- package/dist/mailchimp-KKNF6QJ7.js.map +1 -0
- package/dist/matrix-QVHG76I7.js +279 -0
- package/dist/matrix-QVHG76I7.js.map +1 -0
- package/dist/{mcp-LS7Q3Z5W.js → mcp-3JI6W7ZE.js} +3 -3
- package/dist/mcp-3JI6W7ZE.js.map +1 -0
- package/dist/microsoft365-UCBKJHNX.js +164 -0
- package/dist/microsoft365-UCBKJHNX.js.map +1 -0
- package/dist/ocr-AC7NPX33.js +22 -0
- package/dist/ocr-AC7NPX33.js.map +1 -0
- package/dist/ollama-BOAMSPLJ.js +8 -0
- package/dist/ollama-BOAMSPLJ.js.map +1 -0
- package/dist/pages-MI523RB7.js +26 -0
- package/dist/pages-MI523RB7.js.map +1 -0
- package/dist/pair-JDFTERIK.js +24 -0
- package/dist/pair-JDFTERIK.js.map +1 -0
- package/dist/pairing-IFQYCPNS.js +10 -0
- package/dist/pairing-IFQYCPNS.js.map +1 -0
- package/dist/pdf-ALQVOEJR.js +17 -0
- package/dist/pdf-ALQVOEJR.js.map +1 -0
- package/dist/presentations-DSV5IHG5.js +1002 -0
- package/dist/presentations-DSV5IHG5.js.map +1 -0
- package/dist/prometheus-JNT2BD4L.js +10 -0
- package/dist/prometheus-JNT2BD4L.js.map +1 -0
- package/dist/providers-J4LYPHDR.js +19 -0
- package/dist/providers-J4LYPHDR.js.map +1 -0
- package/dist/qr-code-WIX4PB4U.js +16 -0
- package/dist/qr-code-WIX4PB4U.js.map +1 -0
- package/dist/quickbooks-XB4NII2S.js +190 -0
- package/dist/quickbooks-XB4NII2S.js.map +1 -0
- package/dist/regex-tool-W4ABRKGK.js +24 -0
- package/dist/regex-tool-W4ABRKGK.js.map +1 -0
- package/dist/scheduler-VK4WFERV.js +63 -0
- package/dist/scheduler-VK4WFERV.js.map +1 -0
- package/dist/search-BCLBO5E3.js +25 -0
- package/dist/search-BCLBO5E3.js.map +1 -0
- package/dist/sendgrid-RNXCAFKM.js +152 -0
- package/dist/sendgrid-RNXCAFKM.js.map +1 -0
- package/dist/shopify-NCXYJB4R.js +171 -0
- package/dist/shopify-NCXYJB4R.js.map +1 -0
- package/dist/signal-6CGDFYL2.js +35 -0
- package/dist/signal-6CGDFYL2.js.map +1 -0
- package/dist/slack-IZQWIKOH.js +75 -0
- package/dist/slack-IZQWIKOH.js.map +1 -0
- package/dist/sms-M3JIOTCW.js +23 -0
- package/dist/sms-M3JIOTCW.js.map +1 -0
- package/dist/{src-K7GASHRH.js → src-VYUE6LRA.js} +138 -32
- package/dist/src-VYUE6LRA.js.map +1 -0
- package/dist/stocks-XXWBPOCU.js +14 -0
- package/dist/stocks-XXWBPOCU.js.map +1 -0
- package/dist/text-transform-6SGUA5Z4.js +22 -0
- package/dist/text-transform-6SGUA5Z4.js.map +1 -0
- package/dist/tools-2RLEI2N6.js +38 -0
- package/dist/tools-2RLEI2N6.js.map +1 -0
- package/dist/tunnel-IWMXUML4.js +301 -0
- package/dist/tunnel-IWMXUML4.js.map +1 -0
- package/dist/twilio-53GEW5JT.js +139 -0
- package/dist/twilio-53GEW5JT.js.map +1 -0
- package/dist/unit-converter-ZYXMEZOE.js +14 -0
- package/dist/unit-converter-ZYXMEZOE.js.map +1 -0
- package/dist/whatsapp-LFX6YKCM.js +35 -0
- package/dist/whatsapp-LFX6YKCM.js.map +1 -0
- package/dist/word-document-7B6SJMAY.js +902 -0
- package/dist/word-document-7B6SJMAY.js.map +1 -0
- package/dist/xero-QYO66D45.js +162 -0
- package/dist/xero-QYO66D45.js.map +1 -0
- package/dist/zapier-webhook-TBZ5YF2A.js +106 -0
- package/dist/zapier-webhook-TBZ5YF2A.js.map +1 -0
- package/drizzle/0002_mushy_master_mold.sql +140 -0
- package/drizzle/meta/0002_snapshot.json +3637 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +100 -98
- package/dist/bot-KJ26BG56.js +0 -15
- package/dist/chunk-4LVWXUNC.js.map +0 -1
- package/dist/chunk-4TG2IG5K.js +0 -5249
- package/dist/chunk-4TG2IG5K.js.map +0 -1
- package/dist/chunk-6DRDKB45.js.map +0 -1
- package/dist/chunk-CI6Q63MM.js +0 -1613
- package/dist/chunk-CI6Q63MM.js.map +0 -1
- package/dist/chunk-KHNYJY2Z.js +0 -178
- package/dist/chunk-KHNYJY2Z.js.map +0 -1
- package/dist/chunk-NSBPE2FW.js +0 -17
- package/dist/discord-ZOJFTVTB.js +0 -49
- package/dist/imessage-JFRB6EJ7.js +0 -14
- package/dist/scheduler-EZ7CZMCS.js +0 -42
- package/dist/signal-T3MCSULM.js +0 -14
- package/dist/slack-N2M4FHAJ.js +0 -54
- package/dist/src-K7GASHRH.js.map +0 -1
- package/dist/tools-24GZHYRF.js +0 -16
- package/dist/whatsapp-VCRUPAO5.js +0 -14
- /package/dist/{bot-KJ26BG56.js.map → audit-logger-OBPR7CRO.js.map} +0 -0
- /package/dist/{chunk-NSBPE2FW.js.map → auth-UOX5K2BE.js.map} +0 -0
- /package/dist/{discord-ZOJFTVTB.js.map → backup-restore-PZ7CYYB7.js.map} +0 -0
- /package/dist/{imessage-JFRB6EJ7.js.map → blocks-R3PODY47.js.map} +0 -0
- /package/dist/{mcp-LS7Q3Z5W.js.map → bot-QRARP4UN.js.map} +0 -0
- /package/dist/{scheduler-EZ7CZMCS.js.map → brain-7XLLM3KC.js.map} +0 -0
- /package/dist/{charts-MMXM6BWW.js.map → charts-V7ARZNKF.js.map} +0 -0
- /package/dist/{chunk-L3PDU3XN.js.map → chunk-4UOE5TUZ.js.map} +0 -0
- /package/dist/{chunk-6SNHU3CY.js.map → chunk-66OJ3WB4.js.map} +0 -0
- /package/dist/{chunk-F6QUZQGI.js.map → chunk-MXAPLSJ5.js.map} +0 -0
- /package/dist/{chunk-GK3E2I7A.js.map → chunk-NHMBTUMW.js.map} +0 -0
- /package/dist/{signal-T3MCSULM.js.map → chunk-PLDDJCW6.js.map} +0 -0
- /package/dist/{chunk-GVJVEWHI.js.map → chunk-SJSUSJ47.js.map} +0 -0
- /package/dist/{chunk-HH2HBTQM.js.map → chunk-TYAGMJNV.js.map} +0 -0
- /package/dist/{chunk-JXUP2X7V.js.map → chunk-VEHFVBLI.js.map} +0 -0
- /package/dist/{slack-N2M4FHAJ.js.map → client-ZQSFPMOB.js.map} +0 -0
- /package/dist/{tools-24GZHYRF.js.map → clipboard-manager-TEO2GEDN.js.map} +0 -0
- /package/dist/{whatsapp-VCRUPAO5.js.map → cron-explain-HHQKPD3M.js.map} +0 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import {
|
|
2
|
+
auditLogs,
|
|
3
|
+
db
|
|
4
|
+
} from "./chunk-XKYRH4FM.js";
|
|
5
|
+
import {
|
|
6
|
+
env
|
|
7
|
+
} from "./chunk-ZLZKF2PM.js";
|
|
8
|
+
|
|
9
|
+
// src/core/security/audit-logger.ts
|
|
10
|
+
import { createHmac, randomUUID } from "crypto";
|
|
11
|
+
import { eq, and, gte, lte, desc, count, min, max, asc } from "drizzle-orm";
|
|
12
|
+
var _cachedSigningKey = null;
|
|
13
|
+
function getAuditSigningKey() {
|
|
14
|
+
if (_cachedSigningKey) return _cachedSigningKey;
|
|
15
|
+
if (env.AUDIT_SIGNING_KEY) {
|
|
16
|
+
_cachedSigningKey = env.AUDIT_SIGNING_KEY;
|
|
17
|
+
return _cachedSigningKey;
|
|
18
|
+
}
|
|
19
|
+
_cachedSigningKey = randomUUID();
|
|
20
|
+
console.warn(
|
|
21
|
+
"[audit-logger] AUDIT_SIGNING_KEY not set \u2014 using a random ephemeral key. Set AUDIT_SIGNING_KEY in .env for persistent tamper-proof audit chains."
|
|
22
|
+
);
|
|
23
|
+
return _cachedSigningKey;
|
|
24
|
+
}
|
|
25
|
+
function signAuditEntry(sequenceNumber, action, userId, resource, detailsJson, timestamp, previousHash) {
|
|
26
|
+
const key = getAuditSigningKey();
|
|
27
|
+
const data = [
|
|
28
|
+
String(sequenceNumber),
|
|
29
|
+
action,
|
|
30
|
+
userId ?? "",
|
|
31
|
+
resource ?? "",
|
|
32
|
+
detailsJson,
|
|
33
|
+
timestamp,
|
|
34
|
+
previousHash ?? ""
|
|
35
|
+
].join("|");
|
|
36
|
+
return createHmac("sha256", key).update(data).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
async function getLastAuditEntry() {
|
|
39
|
+
const [last] = await db.select({
|
|
40
|
+
sequenceNumber: auditLogs.sequenceNumber,
|
|
41
|
+
entryHash: auditLogs.entryHash
|
|
42
|
+
}).from(auditLogs).orderBy(desc(auditLogs.sequenceNumber)).limit(1);
|
|
43
|
+
if (!last || last.sequenceNumber == null || last.entryHash == null) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
sequenceNumber: last.sequenceNumber,
|
|
48
|
+
entryHash: last.entryHash
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function logAudit(entry) {
|
|
52
|
+
const last = await getLastAuditEntry();
|
|
53
|
+
const sequenceNumber = (last?.sequenceNumber ?? 0) + 1;
|
|
54
|
+
const previousHash = last?.entryHash ?? null;
|
|
55
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
56
|
+
const detailsJson = entry.details ? JSON.stringify(entry.details) : "{}";
|
|
57
|
+
const entryHash = signAuditEntry(
|
|
58
|
+
sequenceNumber,
|
|
59
|
+
entry.action,
|
|
60
|
+
entry.userId,
|
|
61
|
+
entry.resource,
|
|
62
|
+
detailsJson,
|
|
63
|
+
timestamp,
|
|
64
|
+
previousHash
|
|
65
|
+
);
|
|
66
|
+
const [log] = await db.insert(auditLogs).values({
|
|
67
|
+
userId: entry.userId,
|
|
68
|
+
sessionId: entry.sessionId,
|
|
69
|
+
action: entry.action,
|
|
70
|
+
resource: entry.resource,
|
|
71
|
+
resourceId: entry.resourceId,
|
|
72
|
+
details: entry.details,
|
|
73
|
+
ipAddress: entry.ipAddress,
|
|
74
|
+
userAgent: entry.userAgent,
|
|
75
|
+
success: entry.success ?? true,
|
|
76
|
+
createdAt: new Date(timestamp),
|
|
77
|
+
sequenceNumber,
|
|
78
|
+
entryHash,
|
|
79
|
+
previousHash
|
|
80
|
+
}).returning();
|
|
81
|
+
return log.id;
|
|
82
|
+
}
|
|
83
|
+
async function queryAuditLogs(options = {}) {
|
|
84
|
+
const {
|
|
85
|
+
userId,
|
|
86
|
+
action,
|
|
87
|
+
resource,
|
|
88
|
+
startDate,
|
|
89
|
+
endDate,
|
|
90
|
+
limit = 100,
|
|
91
|
+
offset = 0
|
|
92
|
+
} = options;
|
|
93
|
+
let query = db.select().from(auditLogs);
|
|
94
|
+
const conditions = [];
|
|
95
|
+
if (userId) {
|
|
96
|
+
conditions.push(eq(auditLogs.userId, userId));
|
|
97
|
+
}
|
|
98
|
+
if (action) {
|
|
99
|
+
conditions.push(eq(auditLogs.action, action));
|
|
100
|
+
}
|
|
101
|
+
if (resource) {
|
|
102
|
+
conditions.push(eq(auditLogs.resource, resource));
|
|
103
|
+
}
|
|
104
|
+
if (startDate) {
|
|
105
|
+
conditions.push(gte(auditLogs.createdAt, startDate));
|
|
106
|
+
}
|
|
107
|
+
if (endDate) {
|
|
108
|
+
conditions.push(lte(auditLogs.createdAt, endDate));
|
|
109
|
+
}
|
|
110
|
+
if (conditions.length > 0) {
|
|
111
|
+
query = query.where(and(...conditions));
|
|
112
|
+
}
|
|
113
|
+
const logs = await query.orderBy(desc(auditLogs.createdAt)).limit(limit).offset(offset);
|
|
114
|
+
return logs;
|
|
115
|
+
}
|
|
116
|
+
async function getRecentUserActivity(userId, hours = 24) {
|
|
117
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1e3);
|
|
118
|
+
return db.select().from(auditLogs).where(and(eq(auditLogs.userId, userId), gte(auditLogs.createdAt, since))).orderBy(desc(auditLogs.createdAt)).limit(100);
|
|
119
|
+
}
|
|
120
|
+
async function countActionsByType(userId, startDate, endDate) {
|
|
121
|
+
const logs = await db.select().from(auditLogs).where(
|
|
122
|
+
and(
|
|
123
|
+
eq(auditLogs.userId, userId),
|
|
124
|
+
gte(auditLogs.createdAt, startDate),
|
|
125
|
+
lte(auditLogs.createdAt, endDate)
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
const counts = {};
|
|
129
|
+
for (const log of logs) {
|
|
130
|
+
counts[log.action] = (counts[log.action] || 0) + 1;
|
|
131
|
+
}
|
|
132
|
+
return counts;
|
|
133
|
+
}
|
|
134
|
+
async function verifyAuditChain(options) {
|
|
135
|
+
const fromSequence = options?.fromSequence ?? 1;
|
|
136
|
+
const batchLimit = options?.limit ?? 1e4;
|
|
137
|
+
let expectedPreviousHash = null;
|
|
138
|
+
let expectedSequence = fromSequence;
|
|
139
|
+
if (fromSequence > 1) {
|
|
140
|
+
const [prev] = await db.select({
|
|
141
|
+
sequenceNumber: auditLogs.sequenceNumber,
|
|
142
|
+
entryHash: auditLogs.entryHash
|
|
143
|
+
}).from(auditLogs).where(eq(auditLogs.sequenceNumber, fromSequence - 1)).limit(1);
|
|
144
|
+
expectedPreviousHash = prev?.entryHash ?? null;
|
|
145
|
+
}
|
|
146
|
+
const entries = await db.select().from(auditLogs).where(gte(auditLogs.sequenceNumber, fromSequence)).orderBy(asc(auditLogs.sequenceNumber)).limit(batchLimit);
|
|
147
|
+
const errors = [];
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const seq = entry.sequenceNumber;
|
|
150
|
+
if (seq == null) {
|
|
151
|
+
errors.push({
|
|
152
|
+
sequenceNumber: expectedSequence,
|
|
153
|
+
error: "Missing sequence number"
|
|
154
|
+
});
|
|
155
|
+
expectedSequence++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (seq !== expectedSequence) {
|
|
159
|
+
errors.push({
|
|
160
|
+
sequenceNumber: expectedSequence,
|
|
161
|
+
error: `Sequence gap: expected ${expectedSequence}, got ${seq}`
|
|
162
|
+
});
|
|
163
|
+
expectedSequence = seq;
|
|
164
|
+
}
|
|
165
|
+
if ((entry.previousHash ?? null) !== expectedPreviousHash) {
|
|
166
|
+
errors.push({
|
|
167
|
+
sequenceNumber: seq,
|
|
168
|
+
error: `Previous hash mismatch: expected ${expectedPreviousHash ?? "(null)"}, got ${entry.previousHash ?? "(null)"}`
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const detailsJson = entry.details ? JSON.stringify(entry.details) : "{}";
|
|
172
|
+
const timestamp = entry.createdAt.toISOString();
|
|
173
|
+
const recomputed = signAuditEntry(
|
|
174
|
+
seq,
|
|
175
|
+
entry.action,
|
|
176
|
+
entry.userId ?? void 0,
|
|
177
|
+
entry.resource ?? void 0,
|
|
178
|
+
detailsJson,
|
|
179
|
+
timestamp,
|
|
180
|
+
entry.previousHash
|
|
181
|
+
);
|
|
182
|
+
if (recomputed !== entry.entryHash) {
|
|
183
|
+
errors.push({
|
|
184
|
+
sequenceNumber: seq,
|
|
185
|
+
error: "Entry hash mismatch \u2014 record may have been tampered with"
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
expectedPreviousHash = entry.entryHash;
|
|
189
|
+
expectedSequence = seq + 1;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
valid: errors.length === 0,
|
|
193
|
+
totalChecked: entries.length,
|
|
194
|
+
firstInvalid: errors.length > 0 ? errors[0].sequenceNumber : void 0,
|
|
195
|
+
errors
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async function getAuditChainIntegrity() {
|
|
199
|
+
const [stats] = await db.select({
|
|
200
|
+
totalEntries: count(auditLogs.id),
|
|
201
|
+
oldestEntry: min(auditLogs.createdAt),
|
|
202
|
+
newestEntry: max(auditLogs.createdAt),
|
|
203
|
+
lastSequence: max(auditLogs.sequenceNumber)
|
|
204
|
+
}).from(auditLogs);
|
|
205
|
+
const totalEntries = Number(stats.totalEntries ?? 0);
|
|
206
|
+
const lastSequence = stats.lastSequence ?? 0;
|
|
207
|
+
const verifyFrom = Math.max(1, lastSequence - 999);
|
|
208
|
+
const verification = await verifyAuditChain({
|
|
209
|
+
fromSequence: verifyFrom,
|
|
210
|
+
limit: 1e3
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
totalEntries,
|
|
214
|
+
oldestEntry: stats.oldestEntry ? new Date(stats.oldestEntry) : null,
|
|
215
|
+
newestEntry: stats.newestEntry ? new Date(stats.newestEntry) : null,
|
|
216
|
+
lastVerified: verification.totalChecked,
|
|
217
|
+
chainValid: verification.valid,
|
|
218
|
+
lastSequence
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
var audit = {
|
|
222
|
+
login: (userId, ipAddress, userAgent) => logAudit({
|
|
223
|
+
userId,
|
|
224
|
+
action: "login",
|
|
225
|
+
resource: "session",
|
|
226
|
+
ipAddress,
|
|
227
|
+
userAgent
|
|
228
|
+
}),
|
|
229
|
+
logout: (userId, sessionId) => logAudit({
|
|
230
|
+
userId,
|
|
231
|
+
sessionId,
|
|
232
|
+
action: "logout",
|
|
233
|
+
resource: "session"
|
|
234
|
+
}),
|
|
235
|
+
toolUse: (userId, toolName, input, success) => logAudit({
|
|
236
|
+
userId,
|
|
237
|
+
action: "tool_use",
|
|
238
|
+
resource: "tool",
|
|
239
|
+
resourceId: toolName,
|
|
240
|
+
details: { input },
|
|
241
|
+
success
|
|
242
|
+
}),
|
|
243
|
+
shellExecute: (userId, command, exitCode, durationMs) => logAudit({
|
|
244
|
+
userId,
|
|
245
|
+
action: "shell_execute",
|
|
246
|
+
resource: "shell",
|
|
247
|
+
details: { command, exitCode, durationMs },
|
|
248
|
+
success: exitCode === 0
|
|
249
|
+
}),
|
|
250
|
+
fileAccess: (userId, action, filePath) => logAudit({
|
|
251
|
+
userId,
|
|
252
|
+
action,
|
|
253
|
+
resource: "file",
|
|
254
|
+
resourceId: filePath
|
|
255
|
+
}),
|
|
256
|
+
memoryCreate: (userId, memoryId, memoryType) => logAudit({
|
|
257
|
+
userId,
|
|
258
|
+
action: "memory_create",
|
|
259
|
+
resource: "memory",
|
|
260
|
+
resourceId: memoryId,
|
|
261
|
+
details: { type: memoryType }
|
|
262
|
+
}),
|
|
263
|
+
modeChange: (userId, fromMode, toMode) => logAudit({
|
|
264
|
+
userId,
|
|
265
|
+
action: "mode_change",
|
|
266
|
+
resource: "mode",
|
|
267
|
+
details: { fromMode, toMode }
|
|
268
|
+
}),
|
|
269
|
+
agentSpawn: (userId, agentId, agentType) => logAudit({
|
|
270
|
+
userId,
|
|
271
|
+
action: "agent_spawn",
|
|
272
|
+
resource: "agent",
|
|
273
|
+
resourceId: agentId,
|
|
274
|
+
details: { type: agentType }
|
|
275
|
+
}),
|
|
276
|
+
error: (userId, errorType, message, context) => logAudit({
|
|
277
|
+
userId,
|
|
278
|
+
action: "error",
|
|
279
|
+
details: { errorType, message, ...context },
|
|
280
|
+
success: false
|
|
281
|
+
})
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export {
|
|
285
|
+
logAudit,
|
|
286
|
+
queryAuditLogs,
|
|
287
|
+
getRecentUserActivity,
|
|
288
|
+
countActionsByType,
|
|
289
|
+
verifyAuditChain,
|
|
290
|
+
getAuditChainIntegrity,
|
|
291
|
+
audit
|
|
292
|
+
};
|
|
293
|
+
//# sourceMappingURL=chunk-OCVQGBJK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/security/audit-logger.ts"],"sourcesContent":["import { createHmac, randomUUID } from \"crypto\";\nimport { db } from \"../../db\";\nimport { auditLogs, NewAuditLog } from \"../../db/schema\";\nimport { eq, and, gte, lte, desc, count, min, max, asc } from \"drizzle-orm\";\nimport { env } from \"../../config/env\";\n\nexport type AuditAction =\n | \"login\"\n | \"logout\"\n | \"session_create\"\n | \"session_invalidate\"\n | \"api_key_create\"\n | \"api_key_revoke\"\n | \"tool_use\"\n | \"chat_message\"\n | \"memory_create\"\n | \"memory_delete\"\n | \"memory_archive\"\n | \"settings_change\"\n | \"mode_change\"\n | \"agent_spawn\"\n | \"agent_complete\"\n | \"file_read\"\n | \"file_write\"\n | \"shell_execute\"\n | \"web_browse\"\n | \"error\";\n\nexport type AuditResource =\n | \"session\"\n | \"api_key\"\n | \"tool\"\n | \"chat\"\n | \"memory\"\n | \"settings\"\n | \"mode\"\n | \"agent\"\n | \"file\"\n | \"shell\"\n | \"browser\";\n\nexport interface AuditLogEntry {\n userId?: string;\n sessionId?: string;\n action: AuditAction;\n resource?: AuditResource;\n resourceId?: string;\n details?: Record<string, unknown>;\n ipAddress?: string;\n userAgent?: string;\n success?: boolean;\n}\n\n// --- Tamper-proof chain hashing helpers (SOC 2 compliance) ---\n\nlet _cachedSigningKey: string | null = null;\n\nfunction getAuditSigningKey(): string {\n if (_cachedSigningKey) return _cachedSigningKey;\n if (env.AUDIT_SIGNING_KEY) {\n _cachedSigningKey = env.AUDIT_SIGNING_KEY;\n return _cachedSigningKey;\n }\n _cachedSigningKey = randomUUID();\n console.warn(\n \"[audit-logger] AUDIT_SIGNING_KEY not set — using a random ephemeral key. \" +\n \"Set AUDIT_SIGNING_KEY in .env for persistent tamper-proof audit chains.\"\n );\n return _cachedSigningKey;\n}\n\nfunction signAuditEntry(\n sequenceNumber: number,\n action: string,\n userId: string | undefined,\n resource: string | undefined,\n detailsJson: string,\n timestamp: string,\n previousHash: string | null\n): string {\n const key = getAuditSigningKey();\n const data = [\n String(sequenceNumber),\n action,\n userId ?? \"\",\n resource ?? \"\",\n detailsJson,\n timestamp,\n previousHash ?? \"\",\n ].join(\"|\");\n return createHmac(\"sha256\", key).update(data).digest(\"hex\");\n}\n\nasync function getLastAuditEntry(): Promise<{\n sequenceNumber: number;\n entryHash: string;\n} | null> {\n const [last] = await db\n .select({\n sequenceNumber: auditLogs.sequenceNumber,\n entryHash: auditLogs.entryHash,\n })\n .from(auditLogs)\n .orderBy(desc(auditLogs.sequenceNumber))\n .limit(1);\n\n if (!last || last.sequenceNumber == null || last.entryHash == null) {\n return null;\n }\n\n return {\n sequenceNumber: last.sequenceNumber,\n entryHash: last.entryHash,\n };\n}\n\nexport async function logAudit(entry: AuditLogEntry): Promise<string> {\n // Fetch previous chain entry for tamper-proof linking\n const last = await getLastAuditEntry();\n const sequenceNumber = (last?.sequenceNumber ?? 0) + 1;\n const previousHash = last?.entryHash ?? null;\n\n const timestamp = new Date().toISOString();\n const detailsJson = entry.details ? JSON.stringify(entry.details) : \"{}\";\n\n const entryHash = signAuditEntry(\n sequenceNumber,\n entry.action,\n entry.userId,\n entry.resource,\n detailsJson,\n timestamp,\n previousHash\n );\n\n const [log] = await db\n .insert(auditLogs)\n .values({\n userId: entry.userId,\n sessionId: entry.sessionId,\n action: entry.action,\n resource: entry.resource,\n resourceId: entry.resourceId,\n details: entry.details,\n ipAddress: entry.ipAddress,\n userAgent: entry.userAgent,\n success: entry.success ?? true,\n createdAt: new Date(timestamp),\n sequenceNumber,\n entryHash,\n previousHash,\n })\n .returning();\n\n return log.id;\n}\n\nexport interface AuditQueryOptions {\n userId?: string;\n action?: AuditAction;\n resource?: AuditResource;\n startDate?: Date;\n endDate?: Date;\n limit?: number;\n offset?: number;\n}\n\nexport async function queryAuditLogs(options: AuditQueryOptions = {}) {\n const {\n userId,\n action,\n resource,\n startDate,\n endDate,\n limit = 100,\n offset = 0,\n } = options;\n\n let query = db.select().from(auditLogs);\n\n const conditions = [];\n\n if (userId) {\n conditions.push(eq(auditLogs.userId, userId));\n }\n\n if (action) {\n conditions.push(eq(auditLogs.action, action));\n }\n\n if (resource) {\n conditions.push(eq(auditLogs.resource, resource));\n }\n\n if (startDate) {\n conditions.push(gte(auditLogs.createdAt, startDate));\n }\n\n if (endDate) {\n conditions.push(lte(auditLogs.createdAt, endDate));\n }\n\n if (conditions.length > 0) {\n query = query.where(and(...conditions)) as typeof query;\n }\n\n const logs = await query\n .orderBy(desc(auditLogs.createdAt))\n .limit(limit)\n .offset(offset);\n\n return logs;\n}\n\nexport async function getRecentUserActivity(\n userId: string,\n hours = 24\n): Promise<typeof auditLogs.$inferSelect[]> {\n const since = new Date(Date.now() - hours * 60 * 60 * 1000);\n\n return db\n .select()\n .from(auditLogs)\n .where(and(eq(auditLogs.userId, userId), gte(auditLogs.createdAt, since)))\n .orderBy(desc(auditLogs.createdAt))\n .limit(100);\n}\n\nexport async function countActionsByType(\n userId: string,\n startDate: Date,\n endDate: Date\n): Promise<Record<string, number>> {\n const logs = await db\n .select()\n .from(auditLogs)\n .where(\n and(\n eq(auditLogs.userId, userId),\n gte(auditLogs.createdAt, startDate),\n lte(auditLogs.createdAt, endDate)\n )\n );\n\n const counts: Record<string, number> = {};\n for (const log of logs) {\n counts[log.action] = (counts[log.action] || 0) + 1;\n }\n\n return counts;\n}\n\n// --- Audit chain verification (SOC 2 compliance) ---\n\nexport async function verifyAuditChain(\n options?: { fromSequence?: number; limit?: number }\n): Promise<{\n valid: boolean;\n totalChecked: number;\n firstInvalid?: number;\n errors: Array<{ sequenceNumber: number; error: string }>;\n}> {\n const fromSequence = options?.fromSequence ?? 1;\n const batchLimit = options?.limit ?? 10000;\n\n // Fetch the entry just before fromSequence to get its hash for linkage check\n let expectedPreviousHash: string | null = null;\n let expectedSequence = fromSequence;\n\n if (fromSequence > 1) {\n const [prev] = await db\n .select({\n sequenceNumber: auditLogs.sequenceNumber,\n entryHash: auditLogs.entryHash,\n })\n .from(auditLogs)\n .where(eq(auditLogs.sequenceNumber, fromSequence - 1))\n .limit(1);\n\n expectedPreviousHash = prev?.entryHash ?? null;\n }\n\n const entries = await db\n .select()\n .from(auditLogs)\n .where(gte(auditLogs.sequenceNumber, fromSequence))\n .orderBy(asc(auditLogs.sequenceNumber))\n .limit(batchLimit);\n\n const errors: Array<{ sequenceNumber: number; error: string }> = [];\n\n for (const entry of entries) {\n const seq = entry.sequenceNumber;\n\n if (seq == null) {\n errors.push({\n sequenceNumber: expectedSequence,\n error: \"Missing sequence number\",\n });\n expectedSequence++;\n continue;\n }\n\n // Check sequence continuity\n if (seq !== expectedSequence) {\n errors.push({\n sequenceNumber: expectedSequence,\n error: `Sequence gap: expected ${expectedSequence}, got ${seq}`,\n });\n expectedSequence = seq; // re-sync\n }\n\n // Check previousHash linkage\n if ((entry.previousHash ?? null) !== expectedPreviousHash) {\n errors.push({\n sequenceNumber: seq,\n error: `Previous hash mismatch: expected ${expectedPreviousHash ?? \"(null)\"}, got ${entry.previousHash ?? \"(null)\"}`,\n });\n }\n\n // Recompute and verify entryHash\n const detailsJson = entry.details ? JSON.stringify(entry.details) : \"{}\";\n const timestamp = entry.createdAt.toISOString();\n\n const recomputed = signAuditEntry(\n seq,\n entry.action,\n entry.userId ?? undefined,\n entry.resource ?? undefined,\n detailsJson,\n timestamp,\n entry.previousHash\n );\n\n if (recomputed !== entry.entryHash) {\n errors.push({\n sequenceNumber: seq,\n error: \"Entry hash mismatch — record may have been tampered with\",\n });\n }\n\n // Advance expectations\n expectedPreviousHash = entry.entryHash;\n expectedSequence = seq + 1;\n }\n\n return {\n valid: errors.length === 0,\n totalChecked: entries.length,\n firstInvalid: errors.length > 0 ? errors[0].sequenceNumber : undefined,\n errors,\n };\n}\n\nexport async function getAuditChainIntegrity(): Promise<{\n totalEntries: number;\n oldestEntry: Date | null;\n newestEntry: Date | null;\n lastVerified: number;\n chainValid: boolean;\n lastSequence: number;\n}> {\n const [stats] = await db\n .select({\n totalEntries: count(auditLogs.id),\n oldestEntry: min(auditLogs.createdAt),\n newestEntry: max(auditLogs.createdAt),\n lastSequence: max(auditLogs.sequenceNumber),\n })\n .from(auditLogs);\n\n const totalEntries = Number(stats.totalEntries ?? 0);\n const lastSequence = stats.lastSequence ?? 0;\n\n // Verify the last 1000 entries\n const verifyFrom = Math.max(1, lastSequence - 999);\n const verification = await verifyAuditChain({\n fromSequence: verifyFrom,\n limit: 1000,\n });\n\n return {\n totalEntries,\n oldestEntry: stats.oldestEntry ? new Date(stats.oldestEntry) : null,\n newestEntry: stats.newestEntry ? new Date(stats.newestEntry) : null,\n lastVerified: verification.totalChecked,\n chainValid: verification.valid,\n lastSequence,\n };\n}\n\n// Convenience functions for common audit events\nexport const audit = {\n login: (userId: string, ipAddress?: string, userAgent?: string) =>\n logAudit({\n userId,\n action: \"login\",\n resource: \"session\",\n ipAddress,\n userAgent,\n }),\n\n logout: (userId: string, sessionId: string) =>\n logAudit({\n userId,\n sessionId,\n action: \"logout\",\n resource: \"session\",\n }),\n\n toolUse: (\n userId: string,\n toolName: string,\n input: Record<string, unknown>,\n success: boolean\n ) =>\n logAudit({\n userId,\n action: \"tool_use\",\n resource: \"tool\",\n resourceId: toolName,\n details: { input },\n success,\n }),\n\n shellExecute: (\n userId: string,\n command: string,\n exitCode: number,\n durationMs: number\n ) =>\n logAudit({\n userId,\n action: \"shell_execute\",\n resource: \"shell\",\n details: { command, exitCode, durationMs },\n success: exitCode === 0,\n }),\n\n fileAccess: (\n userId: string,\n action: \"file_read\" | \"file_write\",\n filePath: string\n ) =>\n logAudit({\n userId,\n action,\n resource: \"file\",\n resourceId: filePath,\n }),\n\n memoryCreate: (userId: string, memoryId: string, memoryType: string) =>\n logAudit({\n userId,\n action: \"memory_create\",\n resource: \"memory\",\n resourceId: memoryId,\n details: { type: memoryType },\n }),\n\n modeChange: (\n userId: string,\n fromMode: string | null,\n toMode: string\n ) =>\n logAudit({\n userId,\n action: \"mode_change\",\n resource: \"mode\",\n details: { fromMode, toMode },\n }),\n\n agentSpawn: (userId: string, agentId: string, agentType: string) =>\n logAudit({\n userId,\n action: \"agent_spawn\",\n resource: \"agent\",\n resourceId: agentId,\n details: { type: agentType },\n }),\n\n error: (\n userId: string | undefined,\n errorType: string,\n message: string,\n context?: Record<string, unknown>\n ) =>\n logAudit({\n userId,\n action: \"error\",\n details: { errorType, message, ...context },\n success: false,\n }),\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// src/integrations/spotify/auth.ts
|
|
2
|
+
var SpotifyAuthError = class extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
spotifyError;
|
|
5
|
+
constructor(message, statusCode, spotifyError) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "SpotifyAuthError";
|
|
8
|
+
this.statusCode = statusCode;
|
|
9
|
+
this.spotifyError = spotifyError;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var SPOTIFY_SCOPES = {
|
|
13
|
+
// Images
|
|
14
|
+
UGC_IMAGE_UPLOAD: "ugc-image-upload",
|
|
15
|
+
// Spotify Connect
|
|
16
|
+
USER_READ_PLAYBACK_STATE: "user-read-playback-state",
|
|
17
|
+
USER_MODIFY_PLAYBACK_STATE: "user-modify-playback-state",
|
|
18
|
+
USER_READ_CURRENTLY_PLAYING: "user-read-currently-playing",
|
|
19
|
+
// Playback
|
|
20
|
+
STREAMING: "streaming",
|
|
21
|
+
APP_REMOTE_CONTROL: "app-remote-control",
|
|
22
|
+
// Users
|
|
23
|
+
USER_READ_EMAIL: "user-read-email",
|
|
24
|
+
USER_READ_PRIVATE: "user-read-private",
|
|
25
|
+
// Follow
|
|
26
|
+
USER_FOLLOW_READ: "user-follow-read",
|
|
27
|
+
USER_FOLLOW_MODIFY: "user-follow-modify",
|
|
28
|
+
// Library
|
|
29
|
+
USER_LIBRARY_MODIFY: "user-library-modify",
|
|
30
|
+
USER_LIBRARY_READ: "user-library-read",
|
|
31
|
+
// Listening History
|
|
32
|
+
USER_READ_PLAYBACK_POSITION: "user-read-playback-position",
|
|
33
|
+
USER_TOP_READ: "user-top-read",
|
|
34
|
+
USER_READ_RECENTLY_PLAYED: "user-read-recently-played",
|
|
35
|
+
// Playlists
|
|
36
|
+
PLAYLIST_MODIFY_PRIVATE: "playlist-modify-private",
|
|
37
|
+
PLAYLIST_READ_COLLABORATIVE: "playlist-read-collaborative",
|
|
38
|
+
PLAYLIST_READ_PRIVATE: "playlist-read-private",
|
|
39
|
+
PLAYLIST_MODIFY_PUBLIC: "playlist-modify-public"
|
|
40
|
+
};
|
|
41
|
+
var DEFAULT_SCOPES = [
|
|
42
|
+
SPOTIFY_SCOPES.USER_READ_PLAYBACK_STATE,
|
|
43
|
+
SPOTIFY_SCOPES.USER_MODIFY_PLAYBACK_STATE,
|
|
44
|
+
SPOTIFY_SCOPES.USER_READ_CURRENTLY_PLAYING,
|
|
45
|
+
SPOTIFY_SCOPES.USER_READ_EMAIL,
|
|
46
|
+
SPOTIFY_SCOPES.USER_READ_PRIVATE,
|
|
47
|
+
SPOTIFY_SCOPES.USER_LIBRARY_MODIFY,
|
|
48
|
+
SPOTIFY_SCOPES.USER_LIBRARY_READ,
|
|
49
|
+
SPOTIFY_SCOPES.USER_TOP_READ,
|
|
50
|
+
SPOTIFY_SCOPES.USER_READ_RECENTLY_PLAYED,
|
|
51
|
+
SPOTIFY_SCOPES.PLAYLIST_MODIFY_PRIVATE,
|
|
52
|
+
SPOTIFY_SCOPES.PLAYLIST_READ_COLLABORATIVE,
|
|
53
|
+
SPOTIFY_SCOPES.PLAYLIST_READ_PRIVATE,
|
|
54
|
+
SPOTIFY_SCOPES.PLAYLIST_MODIFY_PUBLIC
|
|
55
|
+
];
|
|
56
|
+
var SPOTIFY_ACCOUNTS_URL = "https://accounts.spotify.com";
|
|
57
|
+
var SPOTIFY_API_URL = "https://api.spotify.com/v1";
|
|
58
|
+
var SpotifyAuth = class {
|
|
59
|
+
config;
|
|
60
|
+
tokens = null;
|
|
61
|
+
tokenRefreshPromise = null;
|
|
62
|
+
constructor(config) {
|
|
63
|
+
this.config = config;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the Base64 encoded client credentials
|
|
67
|
+
*/
|
|
68
|
+
getBasicAuthHeader() {
|
|
69
|
+
const credentials = `${this.config.clientId}:${this.config.clientSecret}`;
|
|
70
|
+
return `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate the authorization URL for the OAuth2 flow
|
|
74
|
+
*/
|
|
75
|
+
getAuthorizationUrl(scopes = DEFAULT_SCOPES, state, showDialog = false) {
|
|
76
|
+
const params = new URLSearchParams({
|
|
77
|
+
client_id: this.config.clientId,
|
|
78
|
+
response_type: "code",
|
|
79
|
+
redirect_uri: this.config.redirectUri,
|
|
80
|
+
scope: scopes.join(" "),
|
|
81
|
+
show_dialog: String(showDialog)
|
|
82
|
+
});
|
|
83
|
+
if (state) {
|
|
84
|
+
params.set("state", state);
|
|
85
|
+
}
|
|
86
|
+
return `${SPOTIFY_ACCOUNTS_URL}/authorize?${params.toString()}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Exchange an authorization code for tokens
|
|
90
|
+
*/
|
|
91
|
+
async exchangeCode(code) {
|
|
92
|
+
const response = await fetch(`${SPOTIFY_ACCOUNTS_URL}/api/token`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: this.getBasicAuthHeader(),
|
|
96
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
97
|
+
},
|
|
98
|
+
body: new URLSearchParams({
|
|
99
|
+
grant_type: "authorization_code",
|
|
100
|
+
code,
|
|
101
|
+
redirect_uri: this.config.redirectUri
|
|
102
|
+
})
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
const error = await this.parseErrorResponse(response);
|
|
106
|
+
throw new SpotifyAuthError(
|
|
107
|
+
`Failed to exchange code: ${error.message}`,
|
|
108
|
+
response.status,
|
|
109
|
+
error
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const data = await response.json();
|
|
113
|
+
this.tokens = this.parseTokenResponse(data);
|
|
114
|
+
return this.tokens;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Refresh the access token using the refresh token
|
|
118
|
+
*/
|
|
119
|
+
async refreshAccessToken(refreshToken) {
|
|
120
|
+
const tokenToRefresh = refreshToken ?? this.tokens?.refreshToken;
|
|
121
|
+
if (!tokenToRefresh) {
|
|
122
|
+
throw new SpotifyAuthError("No refresh token available");
|
|
123
|
+
}
|
|
124
|
+
if (this.tokenRefreshPromise) {
|
|
125
|
+
return this.tokenRefreshPromise;
|
|
126
|
+
}
|
|
127
|
+
this.tokenRefreshPromise = this.performTokenRefresh(tokenToRefresh);
|
|
128
|
+
try {
|
|
129
|
+
const tokens = await this.tokenRefreshPromise;
|
|
130
|
+
return tokens;
|
|
131
|
+
} finally {
|
|
132
|
+
this.tokenRefreshPromise = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async performTokenRefresh(refreshToken) {
|
|
136
|
+
const response = await fetch(`${SPOTIFY_ACCOUNTS_URL}/api/token`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: this.getBasicAuthHeader(),
|
|
140
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
141
|
+
},
|
|
142
|
+
body: new URLSearchParams({
|
|
143
|
+
grant_type: "refresh_token",
|
|
144
|
+
refresh_token: refreshToken
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const error = await this.parseErrorResponse(response);
|
|
149
|
+
throw new SpotifyAuthError(
|
|
150
|
+
`Failed to refresh token: ${error.message}`,
|
|
151
|
+
response.status,
|
|
152
|
+
error
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const data = await response.json();
|
|
156
|
+
this.tokens = {
|
|
157
|
+
...this.parseTokenResponse(data),
|
|
158
|
+
refreshToken: data.refresh_token ?? refreshToken
|
|
159
|
+
};
|
|
160
|
+
return this.tokens;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Parse the token response into our internal format
|
|
164
|
+
*/
|
|
165
|
+
parseTokenResponse(data) {
|
|
166
|
+
return {
|
|
167
|
+
accessToken: data.access_token,
|
|
168
|
+
tokenType: data.token_type,
|
|
169
|
+
scope: data.scope,
|
|
170
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
171
|
+
refreshToken: data.refresh_token ?? ""
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Parse an error response from Spotify
|
|
176
|
+
*/
|
|
177
|
+
async parseErrorResponse(response) {
|
|
178
|
+
try {
|
|
179
|
+
const data = await response.json();
|
|
180
|
+
if (data.error) {
|
|
181
|
+
return data.error;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
status: response.status,
|
|
185
|
+
message: data.error_description ?? response.statusText
|
|
186
|
+
};
|
|
187
|
+
} catch {
|
|
188
|
+
return {
|
|
189
|
+
status: response.status,
|
|
190
|
+
message: response.statusText
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Check if the current access token is expired (or about to expire)
|
|
196
|
+
*/
|
|
197
|
+
isTokenExpired(bufferMs = 6e4) {
|
|
198
|
+
if (!this.tokens) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return Date.now() >= this.tokens.expiresAt - bufferMs;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get a valid access token, refreshing if necessary
|
|
205
|
+
*/
|
|
206
|
+
async getAccessToken() {
|
|
207
|
+
if (!this.tokens) {
|
|
208
|
+
throw new SpotifyAuthError("Not authenticated. Please authenticate first.");
|
|
209
|
+
}
|
|
210
|
+
if (this.isTokenExpired()) {
|
|
211
|
+
await this.refreshAccessToken();
|
|
212
|
+
}
|
|
213
|
+
return this.tokens.accessToken;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get the current tokens
|
|
217
|
+
*/
|
|
218
|
+
getTokens() {
|
|
219
|
+
return this.tokens;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Set tokens (useful for restoring from storage)
|
|
223
|
+
*/
|
|
224
|
+
setTokens(tokens) {
|
|
225
|
+
this.tokens = tokens;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Clear stored tokens
|
|
229
|
+
*/
|
|
230
|
+
clearTokens() {
|
|
231
|
+
this.tokens = null;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Check if authenticated
|
|
235
|
+
*/
|
|
236
|
+
isAuthenticated() {
|
|
237
|
+
return this.tokens !== null && !!this.tokens.refreshToken;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Make an authenticated request to the Spotify API
|
|
241
|
+
*/
|
|
242
|
+
async request(endpoint, options = {}) {
|
|
243
|
+
const accessToken = await this.getAccessToken();
|
|
244
|
+
const url = endpoint.startsWith("http") ? endpoint : `${SPOTIFY_API_URL}${endpoint}`;
|
|
245
|
+
const response = await fetch(url, {
|
|
246
|
+
...options,
|
|
247
|
+
headers: {
|
|
248
|
+
Authorization: `Bearer ${accessToken}`,
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
...options.headers
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
if (response.status === 204) {
|
|
254
|
+
return void 0;
|
|
255
|
+
}
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
if (response.status === 401) {
|
|
258
|
+
await this.refreshAccessToken();
|
|
259
|
+
return this.request(endpoint, options);
|
|
260
|
+
}
|
|
261
|
+
const error = await this.parseErrorResponse(response);
|
|
262
|
+
throw new SpotifyAuthError(
|
|
263
|
+
`API request failed: ${error.message}`,
|
|
264
|
+
response.status,
|
|
265
|
+
error
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const text = await response.text();
|
|
269
|
+
if (!text) {
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
return JSON.parse(text);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* GET request helper
|
|
276
|
+
*/
|
|
277
|
+
async get(endpoint, params) {
|
|
278
|
+
let url = endpoint;
|
|
279
|
+
if (params) {
|
|
280
|
+
const searchParams = new URLSearchParams(params);
|
|
281
|
+
url = `${endpoint}?${searchParams.toString()}`;
|
|
282
|
+
}
|
|
283
|
+
return this.request(url, { method: "GET" });
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* POST request helper
|
|
287
|
+
*/
|
|
288
|
+
async post(endpoint, body) {
|
|
289
|
+
return this.request(endpoint, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
body: body ? JSON.stringify(body) : void 0
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* PUT request helper
|
|
296
|
+
*/
|
|
297
|
+
async put(endpoint, body) {
|
|
298
|
+
return this.request(endpoint, {
|
|
299
|
+
method: "PUT",
|
|
300
|
+
body: body ? JSON.stringify(body) : void 0
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* DELETE request helper
|
|
305
|
+
*/
|
|
306
|
+
async delete(endpoint, body) {
|
|
307
|
+
return this.request(endpoint, {
|
|
308
|
+
method: "DELETE",
|
|
309
|
+
body: body ? JSON.stringify(body) : void 0
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get the current user's profile
|
|
314
|
+
*/
|
|
315
|
+
async getCurrentUser() {
|
|
316
|
+
return this.get("/me");
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
function createSpotifyAuth(config) {
|
|
320
|
+
return new SpotifyAuth(config);
|
|
321
|
+
}
|
|
322
|
+
var auth_default = SpotifyAuth;
|
|
323
|
+
|
|
324
|
+
export {
|
|
325
|
+
SpotifyAuthError,
|
|
326
|
+
SPOTIFY_SCOPES,
|
|
327
|
+
DEFAULT_SCOPES,
|
|
328
|
+
SpotifyAuth,
|
|
329
|
+
createSpotifyAuth,
|
|
330
|
+
auth_default
|
|
331
|
+
};
|
|
332
|
+
//# sourceMappingURL=chunk-P6QINGFL.js.map
|