@rewind-ai/cli 0.6.2
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/.env.example +8 -0
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/src/action/capture.d.ts +15 -0
- package/dist/src/action/capture.js +215 -0
- package/dist/src/action/capture.js.map +1 -0
- package/dist/src/action/mediate.d.ts +28 -0
- package/dist/src/action/mediate.js +109 -0
- package/dist/src/action/mediate.js.map +1 -0
- package/dist/src/action/mutate.d.ts +11 -0
- package/dist/src/action/mutate.js +189 -0
- package/dist/src/action/mutate.js.map +1 -0
- package/dist/src/action/objects.d.ts +5 -0
- package/dist/src/action/objects.js +50 -0
- package/dist/src/action/objects.js.map +1 -0
- package/dist/src/action/store.d.ts +24 -0
- package/dist/src/action/store.js +149 -0
- package/dist/src/action/store.js.map +1 -0
- package/dist/src/action/types.d.ts +84 -0
- package/dist/src/action/types.js +3 -0
- package/dist/src/action/types.js.map +1 -0
- package/dist/src/action/undo.d.ts +11 -0
- package/dist/src/action/undo.js +42 -0
- package/dist/src/action/undo.js.map +1 -0
- package/dist/src/adapters/filesystem.d.ts +35 -0
- package/dist/src/adapters/filesystem.js +499 -0
- package/dist/src/adapters/filesystem.js.map +1 -0
- package/dist/src/adapters/index.d.ts +7 -0
- package/dist/src/adapters/index.js +19 -0
- package/dist/src/adapters/index.js.map +1 -0
- package/dist/src/adapters/types.d.ts +54 -0
- package/dist/src/adapters/types.js +7 -0
- package/dist/src/adapters/types.js.map +1 -0
- package/dist/src/api/server.d.ts +25 -0
- package/dist/src/api/server.js +786 -0
- package/dist/src/api/server.js.map +1 -0
- package/dist/src/approval/store.d.ts +35 -0
- package/dist/src/approval/store.js +113 -0
- package/dist/src/approval/store.js.map +1 -0
- package/dist/src/checkpoint.d.ts +9 -0
- package/dist/src/checkpoint.js +140 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/cli/v5.d.ts +15 -0
- package/dist/src/cli/v5.js +110 -0
- package/dist/src/cli/v5.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +823 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/clients/types.d.ts +90 -0
- package/dist/src/clients/types.js +239 -0
- package/dist/src/clients/types.js.map +1 -0
- package/dist/src/commercial/auth/provider.d.ts +61 -0
- package/dist/src/commercial/auth/provider.js +227 -0
- package/dist/src/commercial/auth/provider.js.map +1 -0
- package/dist/src/commercial/entitlement/provider.d.ts +27 -0
- package/dist/src/commercial/entitlement/provider.js +192 -0
- package/dist/src/commercial/entitlement/provider.js.map +1 -0
- package/dist/src/commercial/entitlement/signing.d.ts +61 -0
- package/dist/src/commercial/entitlement/signing.js +124 -0
- package/dist/src/commercial/entitlement/signing.js.map +1 -0
- package/dist/src/commercial/env.d.ts +10 -0
- package/dist/src/commercial/env.js +40 -0
- package/dist/src/commercial/env.js.map +1 -0
- package/dist/src/commercial/product.d.ts +26 -0
- package/dist/src/commercial/product.js +39 -0
- package/dist/src/commercial/product.js.map +1 -0
- package/dist/src/commercial/store.d.ts +46 -0
- package/dist/src/commercial/store.js +104 -0
- package/dist/src/commercial/store.js.map +1 -0
- package/dist/src/commercial/stripe/checkout.d.ts +104 -0
- package/dist/src/commercial/stripe/checkout.js +469 -0
- package/dist/src/commercial/stripe/checkout.js.map +1 -0
- package/dist/src/core/compatibility.d.ts +15 -0
- package/dist/src/core/compatibility.js +226 -0
- package/dist/src/core/compatibility.js.map +1 -0
- package/dist/src/core/engine.d.ts +33 -0
- package/dist/src/core/engine.js +432 -0
- package/dist/src/core/engine.js.map +1 -0
- package/dist/src/core/lifecycle.d.ts +17 -0
- package/dist/src/core/lifecycle.js +56 -0
- package/dist/src/core/lifecycle.js.map +1 -0
- package/dist/src/core/types.d.ts +140 -0
- package/dist/src/core/types.js +37 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/destroy.d.ts +21 -0
- package/dist/src/destroy.js +219 -0
- package/dist/src/destroy.js.map +1 -0
- package/dist/src/diff.d.ts +13 -0
- package/dist/src/diff.js +251 -0
- package/dist/src/diff.js.map +1 -0
- package/dist/src/enforcement/detect.d.ts +13 -0
- package/dist/src/enforcement/detect.js +75 -0
- package/dist/src/enforcement/detect.js.map +1 -0
- package/dist/src/enforcement/hook-policy.d.ts +38 -0
- package/dist/src/enforcement/hook-policy.js +111 -0
- package/dist/src/enforcement/hook-policy.js.map +1 -0
- package/dist/src/enforcement/index.d.ts +8 -0
- package/dist/src/enforcement/index.js +8 -0
- package/dist/src/enforcement/index.js.map +1 -0
- package/dist/src/enforcement/perf.d.ts +6 -0
- package/dist/src/enforcement/perf.js +18 -0
- package/dist/src/enforcement/perf.js.map +1 -0
- package/dist/src/enforcement/policy.d.ts +12 -0
- package/dist/src/enforcement/policy.js +84 -0
- package/dist/src/enforcement/policy.js.map +1 -0
- package/dist/src/enforcement/readonly.d.ts +17 -0
- package/dist/src/enforcement/readonly.js +124 -0
- package/dist/src/enforcement/readonly.js.map +1 -0
- package/dist/src/enforcement/sandbox-clone.d.ts +6 -0
- package/dist/src/enforcement/sandbox-clone.js +11 -0
- package/dist/src/enforcement/sandbox-clone.js.map +1 -0
- package/dist/src/enforcement/setup.d.ts +33 -0
- package/dist/src/enforcement/setup.js +168 -0
- package/dist/src/enforcement/setup.js.map +1 -0
- package/dist/src/enforcement/trust.d.ts +10 -0
- package/dist/src/enforcement/trust.js +71 -0
- package/dist/src/enforcement/trust.js.map +1 -0
- package/dist/src/enforcement/types.d.ts +40 -0
- package/dist/src/enforcement/types.js +3 -0
- package/dist/src/enforcement/types.js.map +1 -0
- package/dist/src/entitlement/provider.d.ts +48 -0
- package/dist/src/entitlement/provider.js +95 -0
- package/dist/src/entitlement/provider.js.map +1 -0
- package/dist/src/hash.d.ts +9 -0
- package/dist/src/hash.js +23 -0
- package/dist/src/hash.js.map +1 -0
- package/dist/src/ledger/query.d.ts +19 -0
- package/dist/src/ledger/query.js +158 -0
- package/dist/src/ledger/query.js.map +1 -0
- package/dist/src/manifest.d.ts +32 -0
- package/dist/src/manifest.js +242 -0
- package/dist/src/manifest.js.map +1 -0
- package/dist/src/mcp/server.d.ts +7 -0
- package/dist/src/mcp/server.js +364 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/paths.d.ts +94 -0
- package/dist/src/paths.js +354 -0
- package/dist/src/paths.js.map +1 -0
- package/dist/src/permission/engine.d.ts +15 -0
- package/dist/src/permission/engine.js +147 -0
- package/dist/src/permission/engine.js.map +1 -0
- package/dist/src/permission/types.d.ts +42 -0
- package/dist/src/permission/types.js +3 -0
- package/dist/src/permission/types.js.map +1 -0
- package/dist/src/product/setup.d.ts +35 -0
- package/dist/src/product/setup.js +300 -0
- package/dist/src/product/setup.js.map +1 -0
- package/dist/src/protect/audit.d.ts +5 -0
- package/dist/src/protect/audit.js +26 -0
- package/dist/src/protect/audit.js.map +1 -0
- package/dist/src/protect/capture.d.ts +7 -0
- package/dist/src/protect/capture.js +250 -0
- package/dist/src/protect/capture.js.map +1 -0
- package/dist/src/protect/fingerprint.d.ts +4 -0
- package/dist/src/protect/fingerprint.js +49 -0
- package/dist/src/protect/fingerprint.js.map +1 -0
- package/dist/src/protect/hook-cli.d.ts +2 -0
- package/dist/src/protect/hook-cli.js +107 -0
- package/dist/src/protect/hook-cli.js.map +1 -0
- package/dist/src/protect/index.d.ts +12 -0
- package/dist/src/protect/index.js +12 -0
- package/dist/src/protect/index.js.map +1 -0
- package/dist/src/protect/install.d.ts +34 -0
- package/dist/src/protect/install.js +293 -0
- package/dist/src/protect/install.js.map +1 -0
- package/dist/src/protect/intercept.d.ts +11 -0
- package/dist/src/protect/intercept.js +390 -0
- package/dist/src/protect/intercept.js.map +1 -0
- package/dist/src/protect/protect.d.ts +17 -0
- package/dist/src/protect/protect.js +194 -0
- package/dist/src/protect/protect.js.map +1 -0
- package/dist/src/protect/root-paths.d.ts +13 -0
- package/dist/src/protect/root-paths.js +140 -0
- package/dist/src/protect/root-paths.js.map +1 -0
- package/dist/src/protect/shell-policy.d.ts +17 -0
- package/dist/src/protect/shell-policy.js +269 -0
- package/dist/src/protect/shell-policy.js.map +1 -0
- package/dist/src/protect/status.d.ts +11 -0
- package/dist/src/protect/status.js +165 -0
- package/dist/src/protect/status.js.map +1 -0
- package/dist/src/protect/store.d.ts +20 -0
- package/dist/src/protect/store.js +170 -0
- package/dist/src/protect/store.js.map +1 -0
- package/dist/src/protect/types.d.ts +116 -0
- package/dist/src/protect/types.js +3 -0
- package/dist/src/protect/types.js.map +1 -0
- package/dist/src/protect/undo-session.d.ts +14 -0
- package/dist/src/protect/undo-session.js +182 -0
- package/dist/src/protect/undo-session.js.map +1 -0
- package/dist/src/restore.d.ts +39 -0
- package/dist/src/restore.js +141 -0
- package/dist/src/restore.js.map +1 -0
- package/dist/src/sandbox.d.ts +23 -0
- package/dist/src/sandbox.js +198 -0
- package/dist/src/sandbox.js.map +1 -0
- package/dist/src/session/ambient.d.ts +10 -0
- package/dist/src/session/ambient.js +53 -0
- package/dist/src/session/ambient.js.map +1 -0
- package/dist/src/session/attribution.d.ts +27 -0
- package/dist/src/session/attribution.js +201 -0
- package/dist/src/session/attribution.js.map +1 -0
- package/dist/src/session/index.d.ts +25 -0
- package/dist/src/session/index.js +472 -0
- package/dist/src/session/index.js.map +1 -0
- package/dist/src/session/process.d.ts +34 -0
- package/dist/src/session/process.js +234 -0
- package/dist/src/session/process.js.map +1 -0
- package/dist/src/session/store.d.ts +14 -0
- package/dist/src/session/store.js +125 -0
- package/dist/src/session/store.js.map +1 -0
- package/dist/src/session/types.d.ts +111 -0
- package/dist/src/session/types.js +3 -0
- package/dist/src/session/types.js.map +1 -0
- package/dist/src/session/watcher.d.ts +32 -0
- package/dist/src/session/watcher.js +186 -0
- package/dist/src/session/watcher.js.map +1 -0
- package/dist/src/timing.d.ts +7 -0
- package/dist/src/timing.js +19 -0
- package/dist/src/timing.js.map +1 -0
- package/dist/src/types.d.ts +101 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/verify.d.ts +8 -0
- package/dist/src/verify.js +126 -0
- package/dist/src/verify.js.map +1 -0
- package/docs/ADAPTERS.md +37 -0
- package/docs/APPROVALS.md +32 -0
- package/docs/ARCHITECTURE.md +57 -0
- package/docs/ATTRIBUTION.md +179 -0
- package/docs/COMMERCIAL.md +51 -0
- package/docs/CURSOR-ENFORCEMENT.md +73 -0
- package/docs/ENFORCEMENT.md +134 -0
- package/docs/FUTURE.md +91 -0
- package/docs/HUMAN-SETUP.md +98 -0
- package/docs/IDENTITY-AUTHORITY.md +35 -0
- package/docs/MCP.md +107 -0
- package/docs/PACKAGING.md +77 -0
- package/docs/PERMISSIONS.md +53 -0
- package/docs/PRODUCT-V5.md +63 -0
- package/docs/PRODUCT-V6.md +69 -0
- package/docs/PROTECTED-SESSIONS.md +155 -0
- package/docs/THREAT-MODEL.md +84 -0
- package/docs/TRANSACTIONS.md +57 -0
- package/package.json +85 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-only REWIND control API for a future desktop app.
|
|
3
|
+
*
|
|
4
|
+
* Security:
|
|
5
|
+
* - Binds to 127.0.0.1 only (never 0.0.0.0)
|
|
6
|
+
* - Requires a session token issued at startup (file + response)
|
|
7
|
+
* - No cloud, no public exposure
|
|
8
|
+
*
|
|
9
|
+
* Threat model: see docs/THREAT-MODEL.md
|
|
10
|
+
*/
|
|
11
|
+
import http from "node:http";
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
13
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { getProjectRoot, getRewindHome } from "../paths.js";
|
|
16
|
+
import { listTransactions, getTransaction, formatTransaction, } from "../ledger/query.js";
|
|
17
|
+
import { approvePending, denyPending, undoTransaction, TransactionEngineError, } from "../core/engine.js";
|
|
18
|
+
import { listPendingApprovals } from "../approval/store.js";
|
|
19
|
+
import { loadPolicy } from "../permission/engine.js";
|
|
20
|
+
import { resolveEntitlementProvider, setEntitlementProvider } from "../entitlement/provider.js";
|
|
21
|
+
import { cliStatus } from "../cli/v5.js";
|
|
22
|
+
import { loadProtectState } from "../protect/index.js";
|
|
23
|
+
import { loadSession } from "../commercial/auth/provider.js";
|
|
24
|
+
import { loadRewindEnv } from "../commercial/env.js";
|
|
25
|
+
import { handleStripeWebhookHttp } from "../commercial/stripe/checkout.js";
|
|
26
|
+
function json(res, status, body) {
|
|
27
|
+
const data = JSON.stringify(body, null, 2);
|
|
28
|
+
res.writeHead(status, {
|
|
29
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
30
|
+
"Cache-Control": "no-store",
|
|
31
|
+
});
|
|
32
|
+
res.end(data);
|
|
33
|
+
}
|
|
34
|
+
function readBody(req) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
req.on("data", (c) => chunks.push(c));
|
|
38
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
39
|
+
req.on("error", reject);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function controlAppHtml() {
|
|
43
|
+
// Local control surface — served from the API itself (no separate bundler).
|
|
44
|
+
return `<!DOCTYPE html>
|
|
45
|
+
<html lang="en">
|
|
46
|
+
<head>
|
|
47
|
+
<meta charset="utf-8"/>
|
|
48
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
49
|
+
<title>REWIND Personal</title>
|
|
50
|
+
<style>
|
|
51
|
+
:root {
|
|
52
|
+
--bg: #141210;
|
|
53
|
+
--fg: #f2ebe3;
|
|
54
|
+
--muted: #9a9086;
|
|
55
|
+
--line: #2c2824;
|
|
56
|
+
--panel: #1a1713;
|
|
57
|
+
--accent: #c4a574;
|
|
58
|
+
--ok: #7d9b76;
|
|
59
|
+
--warn: #c4a574;
|
|
60
|
+
--bad: #c4735f;
|
|
61
|
+
--font: "IBM Plex Sans", "Segoe UI", sans-serif;
|
|
62
|
+
--mono: "IBM Plex Mono", ui-monospace, monospace;
|
|
63
|
+
}
|
|
64
|
+
* { box-sizing: border-box; }
|
|
65
|
+
body {
|
|
66
|
+
margin: 0; background: var(--bg); color: var(--fg);
|
|
67
|
+
font-family: var(--font); min-height: 100vh;
|
|
68
|
+
background-image:
|
|
69
|
+
radial-gradient(ellipse at 20% 0%, #2a241c 0%, transparent 55%),
|
|
70
|
+
linear-gradient(180deg, #181612, #141210);
|
|
71
|
+
}
|
|
72
|
+
header {
|
|
73
|
+
padding: 1.5rem 1.5rem 1rem;
|
|
74
|
+
border-bottom: 1px solid var(--line);
|
|
75
|
+
display: grid;
|
|
76
|
+
gap: 0.35rem;
|
|
77
|
+
}
|
|
78
|
+
header .brand {
|
|
79
|
+
margin: 0; font-size: 1.5rem; letter-spacing: 0.14em;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
}
|
|
82
|
+
header .path {
|
|
83
|
+
margin: 0; color: var(--muted); font-size: 0.85rem;
|
|
84
|
+
font-family: var(--mono); word-break: break-all;
|
|
85
|
+
}
|
|
86
|
+
header .conn {
|
|
87
|
+
margin: 0.15rem 0 0; font-size: 0.8rem; font-family: var(--mono);
|
|
88
|
+
color: var(--ok); letter-spacing: 0.04em;
|
|
89
|
+
}
|
|
90
|
+
header .conn.offline { color: var(--bad); }
|
|
91
|
+
main { padding: 1rem 1.5rem 3rem; max-width: 1100px; }
|
|
92
|
+
nav { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.75rem 0 1.25rem; }
|
|
93
|
+
nav button {
|
|
94
|
+
background: transparent; color: var(--muted); border: 1px solid var(--line);
|
|
95
|
+
padding: 0.4rem 0.7rem; cursor: pointer; font: inherit; font-size: 0.78rem;
|
|
96
|
+
letter-spacing: 0.06em; border-radius: 2px;
|
|
97
|
+
}
|
|
98
|
+
nav button.active { border-color: var(--accent); color: var(--accent); }
|
|
99
|
+
.flash {
|
|
100
|
+
margin-bottom: 1rem; padding: 0.85rem 1rem;
|
|
101
|
+
border: 1px solid var(--bad); background: #241816; color: var(--fg);
|
|
102
|
+
font-family: var(--mono); font-size: 0.8rem; white-space: pre-wrap;
|
|
103
|
+
}
|
|
104
|
+
.flash strong { color: var(--bad); letter-spacing: 0.08em; }
|
|
105
|
+
.layout { display: grid; gap: 1.25rem; }
|
|
106
|
+
@media (min-width: 900px) {
|
|
107
|
+
.layout.split { grid-template-columns: 1.4fr 1fr; align-items: start; }
|
|
108
|
+
}
|
|
109
|
+
.panel {
|
|
110
|
+
border: 1px solid var(--line); background: var(--panel); padding: 0.75rem;
|
|
111
|
+
}
|
|
112
|
+
.panel h2 {
|
|
113
|
+
margin: 0 0 0.75rem; font-size: 0.75rem; letter-spacing: 0.08em;
|
|
114
|
+
color: var(--muted); font-weight: 560;
|
|
115
|
+
}
|
|
116
|
+
table.act {
|
|
117
|
+
width: 100%; border-collapse: collapse; font-size: 0.8rem;
|
|
118
|
+
}
|
|
119
|
+
table.act th {
|
|
120
|
+
text-align: left; color: var(--muted); font-weight: 500;
|
|
121
|
+
font-size: 0.68rem; letter-spacing: 0.05em;
|
|
122
|
+
padding: 0.35rem 0.4rem; border-bottom: 1px solid var(--line);
|
|
123
|
+
}
|
|
124
|
+
table.act td {
|
|
125
|
+
padding: 0.55rem 0.4rem; border-bottom: 1px solid var(--line);
|
|
126
|
+
vertical-align: top; font-family: var(--mono); font-size: 0.72rem;
|
|
127
|
+
}
|
|
128
|
+
table.act tr { cursor: pointer; }
|
|
129
|
+
table.act tr:hover td { background: #201c17; }
|
|
130
|
+
table.act tr.selected td { background: #252019; }
|
|
131
|
+
table.act .muted { color: var(--muted); }
|
|
132
|
+
.tag {
|
|
133
|
+
font-family: var(--mono); font-size: 0.68rem; letter-spacing: 0.04em;
|
|
134
|
+
color: var(--accent);
|
|
135
|
+
}
|
|
136
|
+
.tag.bad { color: var(--bad); }
|
|
137
|
+
.tag.ok { color: var(--ok); }
|
|
138
|
+
.actions { display: flex; gap: 0.35rem; flex-wrap: wrap; }
|
|
139
|
+
.actions button, button.btn {
|
|
140
|
+
font: inherit; font-size: 0.72rem; padding: 0.35rem 0.55rem;
|
|
141
|
+
cursor: pointer; border: 1px solid var(--line); background: #1c1915;
|
|
142
|
+
color: var(--fg); letter-spacing: 0.04em;
|
|
143
|
+
}
|
|
144
|
+
.actions button.primary, button.btn.primary {
|
|
145
|
+
border-color: var(--accent); color: var(--accent);
|
|
146
|
+
}
|
|
147
|
+
.actions button.danger, button.btn.danger {
|
|
148
|
+
border-color: var(--bad); color: var(--bad);
|
|
149
|
+
}
|
|
150
|
+
.dl {
|
|
151
|
+
display: grid; grid-template-columns: 7.5rem 1fr; gap: 0.35rem 0.75rem;
|
|
152
|
+
font-size: 0.8rem; margin: 0;
|
|
153
|
+
}
|
|
154
|
+
.dl dt { color: var(--muted); font-family: var(--mono); font-size: 0.7rem; }
|
|
155
|
+
.dl dd { margin: 0; font-family: var(--mono); font-size: 0.75rem; word-break: break-all; }
|
|
156
|
+
.row {
|
|
157
|
+
display: grid; grid-template-columns: 4.5rem 1fr auto;
|
|
158
|
+
gap: 0.75rem; padding: 0.85rem 0; border-bottom: 1px solid var(--line);
|
|
159
|
+
align-items: start;
|
|
160
|
+
}
|
|
161
|
+
.time { color: var(--muted); font-family: var(--mono); font-size: 0.8rem; }
|
|
162
|
+
.body strong { display: block; font-weight: 560; }
|
|
163
|
+
.body span { color: var(--muted); font-size: 0.85rem; display: block; margin-top: 0.2rem; }
|
|
164
|
+
.rule {
|
|
165
|
+
padding: 0.85rem 0; border-bottom: 1px solid var(--line);
|
|
166
|
+
}
|
|
167
|
+
.rule .effect { font-family: var(--mono); font-size: 0.78rem; letter-spacing: 0.06em; }
|
|
168
|
+
.rule .effect.ALLOW { color: var(--ok); }
|
|
169
|
+
.rule .effect.ASK { color: var(--warn); }
|
|
170
|
+
.rule .effect.DENY { color: var(--bad); }
|
|
171
|
+
.rule .desc { margin-top: 0.35rem; color: var(--fg); font-size: 0.9rem; }
|
|
172
|
+
.rule .when { margin-top: 0.3rem; color: var(--muted); font-family: var(--mono); font-size: 0.72rem; }
|
|
173
|
+
.rule .off { opacity: 0.55; }
|
|
174
|
+
pre {
|
|
175
|
+
white-space: pre-wrap; font-family: var(--mono); font-size: 0.75rem;
|
|
176
|
+
background: #1a1713; padding: 1rem; border: 1px solid var(--line); margin: 0;
|
|
177
|
+
}
|
|
178
|
+
.empty { color: var(--muted); padding: 1rem 0; }
|
|
179
|
+
.note { color: var(--muted); font-size: 0.85rem; margin-top: 0.75rem; }
|
|
180
|
+
</style>
|
|
181
|
+
</head>
|
|
182
|
+
<body>
|
|
183
|
+
<header>
|
|
184
|
+
<h1 class="brand">REWIND</h1>
|
|
185
|
+
<p class="path" id="project">Loading project…</p>
|
|
186
|
+
<p class="conn offline" id="conn">CONNECTING…</p>
|
|
187
|
+
</header>
|
|
188
|
+
<main>
|
|
189
|
+
<nav>
|
|
190
|
+
<button data-view="activity" class="active">ACTIVITY</button>
|
|
191
|
+
<button data-view="approvals">APPROVALS</button>
|
|
192
|
+
<button data-view="agents">AGENTS</button>
|
|
193
|
+
<button data-view="policy">POLICY</button>
|
|
194
|
+
<button data-view="settings">SETTINGS</button>
|
|
195
|
+
</nav>
|
|
196
|
+
<div id="flash"></div>
|
|
197
|
+
<div id="view"></div>
|
|
198
|
+
</main>
|
|
199
|
+
<script>
|
|
200
|
+
const token = new URLSearchParams(location.search).get("token") || localStorage.getItem("rewind_token") || "";
|
|
201
|
+
if (token) localStorage.setItem("rewind_token", token);
|
|
202
|
+
// Preserve token across Stripe redirects that land on public pages then return home
|
|
203
|
+
if (location.pathname === "/" && token) {
|
|
204
|
+
history.replaceState({}, "", "/?token=" + encodeURIComponent(token));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let view = "activity";
|
|
208
|
+
let selectedId = null;
|
|
209
|
+
let lastFlash = "";
|
|
210
|
+
let statusCache = null;
|
|
211
|
+
|
|
212
|
+
async function api(path, opts) {
|
|
213
|
+
opts = opts || {};
|
|
214
|
+
if (!token) {
|
|
215
|
+
const err = new Error("No local API token. Run: rewind open");
|
|
216
|
+
err.status = 401;
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
const res = await fetch(path, {
|
|
220
|
+
...opts,
|
|
221
|
+
headers: {
|
|
222
|
+
"Authorization": "Bearer " + token,
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
...(opts.headers || {}),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
let data = {};
|
|
228
|
+
try { data = await res.json(); } catch (_) {}
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
const err = new Error(data.message || data.error || res.statusText);
|
|
231
|
+
err.status = res.status;
|
|
232
|
+
err.data = data;
|
|
233
|
+
err.conflict = Boolean(data.conflict) || res.status === 409;
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
return data;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function esc(s) {
|
|
240
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
|
241
|
+
return ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c];
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function timeOf(iso) {
|
|
246
|
+
try {
|
|
247
|
+
return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
248
|
+
} catch (_) { return "—"; }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function fullTime(iso) {
|
|
252
|
+
if (!iso) return "—";
|
|
253
|
+
try { return new Date(iso).toLocaleString(); } catch (_) { return String(iso); }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function actorLabel(actor) {
|
|
257
|
+
if (!actor) return "UNKNOWN";
|
|
258
|
+
if (actor.type === "unknown" || actor.client === "UNKNOWN_MCP_CLIENT") return "UNKNOWN";
|
|
259
|
+
const name = actor.client || "UNKNOWN";
|
|
260
|
+
return name;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function agentDisplay(a) {
|
|
264
|
+
const type = String(a.type || "").toLowerCase() === "unknown" ? "UNKNOWN" : (a.type || "—");
|
|
265
|
+
let client = a.client || "UNKNOWN";
|
|
266
|
+
if (String(type).toUpperCase() === "UNKNOWN" || /UNKNOWN_MCP_CLIENT/i.test(client)) {
|
|
267
|
+
client = "UNKNOWN";
|
|
268
|
+
}
|
|
269
|
+
return { client: client, type: type };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function stateHash(state) {
|
|
273
|
+
if (!state) return "—";
|
|
274
|
+
if (state.kind === "filesystem" && state.primary) return state.primary.hash || "—";
|
|
275
|
+
return "—";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function permissionOf(t) {
|
|
279
|
+
return (t.permission && t.permission.decision) || "—";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function tagClass(status) {
|
|
283
|
+
if (status === "CONFLICTED" || status === "DENIED" || status === "FAILED") return "bad";
|
|
284
|
+
if (status === "COMPLETED" || status === "UNDONE") return "ok";
|
|
285
|
+
return "";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function setFlash(html) {
|
|
289
|
+
lastFlash = html || "";
|
|
290
|
+
document.getElementById("flash").innerHTML = lastFlash;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function showConflict(err) {
|
|
294
|
+
const msg = (err && (err.message || (err.data && err.data.message))) || "Undo blocked.";
|
|
295
|
+
setFlash(
|
|
296
|
+
'<div class="flash"><strong>CONFLICT</strong>\\n' +
|
|
297
|
+
esc(msg) +
|
|
298
|
+
'\\n\\nNewer work was preserved. Automatic undo did not overwrite the current state.</div>'
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function ruleWhen(rule) {
|
|
303
|
+
const parts = [];
|
|
304
|
+
if (rule.actorClient) parts.push("actor " + rule.actorClient);
|
|
305
|
+
if (rule.adapter) parts.push("adapter " + rule.adapter);
|
|
306
|
+
if (rule.operation) parts.push("op " + rule.operation);
|
|
307
|
+
if (rule.resourcePattern) parts.push("resource " + rule.resourcePattern);
|
|
308
|
+
if (rule.reversibility) parts.push("reversibility " + rule.reversibility);
|
|
309
|
+
if (rule.risk) parts.push("risk " + rule.risk);
|
|
310
|
+
return parts.length ? parts.join(" · ") : "any mediated operation";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function loadDetail(id) {
|
|
314
|
+
const { transaction: t } = await api("/v1/transactions/" + encodeURIComponent(id));
|
|
315
|
+
const auth = t.authority || {};
|
|
316
|
+
const undo = t.undo || {};
|
|
317
|
+
return (
|
|
318
|
+
'<div class="panel"><h2>TRANSACTION DETAIL</h2>' +
|
|
319
|
+
'<dl class="dl">' +
|
|
320
|
+
'<dt>ID</dt><dd>' + esc(t.id) + '</dd>' +
|
|
321
|
+
'<dt>Actor</dt><dd>' + esc(actorLabel(t.actor)) +
|
|
322
|
+
(t.actor && t.actor.clientVersion ? ' @' + esc(t.actor.clientVersion) : '') +
|
|
323
|
+
(t.actor && t.actor.type ? ' · ' + esc(t.actor.type === "unknown" ? "UNKNOWN" : t.actor.type) : '') +
|
|
324
|
+
'</dd>' +
|
|
325
|
+
'<dt>Authority</dt><dd>' + esc(auth.status || "UNKNOWN") +
|
|
326
|
+
(auth.principal ? ' · ' + esc(auth.principal) : '') +
|
|
327
|
+
'</dd>' +
|
|
328
|
+
'<dt>Source</dt><dd>' + esc(t.source || "—") + '</dd>' +
|
|
329
|
+
'<dt>Adapter</dt><dd>' + esc(t.adapter || "—") + '</dd>' +
|
|
330
|
+
'<dt>Operation</dt><dd>' + esc(t.operation || "—") + '</dd>' +
|
|
331
|
+
'<dt>Target</dt><dd>' + esc((t.target && (t.target.displayName || t.target.resource)) || "—") +
|
|
332
|
+
(t.target && t.target.secondary ? ' → ' + esc(t.target.secondary) : '') +
|
|
333
|
+
'</dd>' +
|
|
334
|
+
'<dt>Before hash</dt><dd>' + esc(stateHash(t.before)) + '</dd>' +
|
|
335
|
+
'<dt>After hash</dt><dd>' + esc(stateHash(t.after)) + '</dd>' +
|
|
336
|
+
'<dt>Permission</dt><dd>' + esc(permissionOf(t)) +
|
|
337
|
+
(t.permission && t.permission.reason ? ' — ' + esc(t.permission.reason) : '') +
|
|
338
|
+
'</dd>' +
|
|
339
|
+
'<dt>Status</dt><dd><span class="tag ' + tagClass(t.status) + '">' + esc(t.status) + '</span></dd>' +
|
|
340
|
+
'<dt>Reversibility</dt><dd>' + esc((t.reversibility && t.reversibility.class) || "—") + '</dd>' +
|
|
341
|
+
'<dt>Undo</dt><dd>' + esc(undo.status || "—") +
|
|
342
|
+
(undo.result ? ' · ' + esc(undo.result) : '') +
|
|
343
|
+
'</dd>' +
|
|
344
|
+
'<dt>Created</dt><dd>' + esc(fullTime(t.createdAt)) + '</dd>' +
|
|
345
|
+
'<dt>Completed</dt><dd>' + esc(fullTime(t.completedAt)) + '</dd>' +
|
|
346
|
+
(undo.conflict && undo.conflict.message
|
|
347
|
+
? '<dt>Conflict</dt><dd class="tag bad">' + esc(undo.conflict.message) + '</dd>'
|
|
348
|
+
: '') +
|
|
349
|
+
(t.error ? '<dt>Error</dt><dd class="tag bad">' + esc(t.error) + '</dd>' : '') +
|
|
350
|
+
'</dl></div>'
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function renderActivity(el) {
|
|
355
|
+
const { transactions } = await api("/v1/transactions?limit=50");
|
|
356
|
+
if (!transactions.length) {
|
|
357
|
+
el.innerHTML = '<div class="empty">No mediated activity yet.</div>';
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const rows = transactions.slice().reverse();
|
|
361
|
+
if (selectedId && !rows.some(function (t) { return t.id === selectedId; })) {
|
|
362
|
+
selectedId = null;
|
|
363
|
+
}
|
|
364
|
+
let html = '<div class="layout' + (selectedId ? ' split' : '') + '">';
|
|
365
|
+
html += '<div class="panel"><h2>ACTIVITY</h2><table class="act"><thead><tr>' +
|
|
366
|
+
'<th>TIME</th><th>ACTOR</th><th>OP</th><th>TARGET</th><th>PERM</th><th>STATUS</th><th>REV</th><th></th>' +
|
|
367
|
+
'</tr></thead><tbody>';
|
|
368
|
+
html += rows.map(function (t) {
|
|
369
|
+
const canUndo = t.status === "COMPLETED" && t.reversibility && t.reversibility.class === "REVERSIBLE";
|
|
370
|
+
const sel = t.id === selectedId ? " selected" : "";
|
|
371
|
+
const undoBtn = canUndo
|
|
372
|
+
? '<button data-undo="' + esc(t.id) + '">UNDO</button>'
|
|
373
|
+
: '';
|
|
374
|
+
return '<tr class="' + sel + '" data-select="' + esc(t.id) + '">' +
|
|
375
|
+
'<td class="muted">' + esc(timeOf(t.createdAt)) + '</td>' +
|
|
376
|
+
'<td>' + esc(actorLabel(t.actor)) + '</td>' +
|
|
377
|
+
'<td>' + esc(t.operation) + '</td>' +
|
|
378
|
+
'<td>' + esc((t.target && (t.target.displayName || t.target.resource)) || "—") + '</td>' +
|
|
379
|
+
'<td>' + esc(permissionOf(t)) + '</td>' +
|
|
380
|
+
'<td><span class="tag ' + tagClass(t.status) + '">' + esc(t.status) + '</span></td>' +
|
|
381
|
+
'<td>' + esc((t.reversibility && t.reversibility.class) || "—") + '</td>' +
|
|
382
|
+
'<td class="actions">' + undoBtn + '</td></tr>';
|
|
383
|
+
}).join("");
|
|
384
|
+
html += '</tbody></table></div>';
|
|
385
|
+
html += '<div id="detail">' + (selectedId ? '' : '<div class="empty">Select a transaction.</div>') + '</div>';
|
|
386
|
+
html += '</div>';
|
|
387
|
+
el.innerHTML = html;
|
|
388
|
+
|
|
389
|
+
if (selectedId) {
|
|
390
|
+
try {
|
|
391
|
+
document.getElementById("detail").innerHTML = await loadDetail(selectedId);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
document.getElementById("detail").innerHTML =
|
|
394
|
+
'<div class="empty">Failed to load detail: ' + esc(e.message) + '</div>';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
el.querySelectorAll("[data-select]").forEach(function (row) {
|
|
399
|
+
row.onclick = function (ev) {
|
|
400
|
+
if (ev.target && ev.target.closest && ev.target.closest("[data-undo]")) return;
|
|
401
|
+
selectedId = row.dataset.select;
|
|
402
|
+
render();
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
el.querySelectorAll("[data-undo]").forEach(function (btn) {
|
|
406
|
+
btn.onclick = async function (ev) {
|
|
407
|
+
ev.stopPropagation();
|
|
408
|
+
btn.disabled = true;
|
|
409
|
+
try {
|
|
410
|
+
await api("/v1/transactions/" + btn.dataset.undo + "/undo", { method: "POST" });
|
|
411
|
+
setFlash("");
|
|
412
|
+
selectedId = btn.dataset.undo;
|
|
413
|
+
} catch (e) {
|
|
414
|
+
if (e.conflict) showConflict(e);
|
|
415
|
+
else setFlash('<div class="flash"><strong>UNDO FAILED</strong>\\n' + esc(e.message) + '</div>');
|
|
416
|
+
}
|
|
417
|
+
render();
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function renderApprovals(el) {
|
|
423
|
+
const { approvals } = await api("/v1/approvals");
|
|
424
|
+
if (!approvals.length) {
|
|
425
|
+
el.innerHTML = '<div class="empty">No pending approvals.</div>';
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
el.innerHTML = approvals.map(function (p) {
|
|
429
|
+
const actor = actorLabel(p.intent && p.intent.actor);
|
|
430
|
+
const op = (p.intent && p.intent.operation) || "—";
|
|
431
|
+
const target = (p.intent && p.intent.target && (p.intent.target.displayName || p.intent.target.resource)) || "—";
|
|
432
|
+
return '<div class="row"><div class="time">' + esc(timeOf(p.createdAt)) + '</div>' +
|
|
433
|
+
'<div class="body"><strong>' + esc(actor) + '</strong>' +
|
|
434
|
+
'<span>' + esc(op) + ' ' + esc(target) + '</span>' +
|
|
435
|
+
'<div class="tag">ASK</div>' +
|
|
436
|
+
(p.reason ? '<span>' + esc(p.reason) + '</span>' : '') +
|
|
437
|
+
'</div><div class="actions">' +
|
|
438
|
+
'<button class="primary" data-approve="' + esc(p.id) + '">ALLOW ONCE</button>' +
|
|
439
|
+
'<button class="danger" data-deny="' + esc(p.id) + '">DENY</button>' +
|
|
440
|
+
'</div></div>';
|
|
441
|
+
}).join("");
|
|
442
|
+
el.querySelectorAll("[data-approve]").forEach(function (btn) {
|
|
443
|
+
btn.onclick = async function () {
|
|
444
|
+
try {
|
|
445
|
+
await api("/v1/approvals/" + btn.dataset.approve + "/approve", { method: "POST" });
|
|
446
|
+
setFlash("");
|
|
447
|
+
} catch (e) { setFlash('<div class="flash"><strong>ERROR</strong>\\n' + esc(e.message) + '</div>'); }
|
|
448
|
+
render();
|
|
449
|
+
};
|
|
450
|
+
});
|
|
451
|
+
el.querySelectorAll("[data-deny]").forEach(function (btn) {
|
|
452
|
+
btn.onclick = async function () {
|
|
453
|
+
try {
|
|
454
|
+
await api("/v1/approvals/" + btn.dataset.deny + "/deny", { method: "POST" });
|
|
455
|
+
setFlash("");
|
|
456
|
+
} catch (e) { setFlash('<div class="flash"><strong>ERROR</strong>\\n' + esc(e.message) + '</div>'); }
|
|
457
|
+
render();
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function renderAgents(el) {
|
|
463
|
+
const { actors } = await api("/v1/actors");
|
|
464
|
+
if (!actors.length) {
|
|
465
|
+
el.innerHTML = '<div class="empty">No actors recorded.</div>';
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
el.innerHTML = '<div class="panel"><h2>AGENTS</h2>' + actors.map(function (a) {
|
|
469
|
+
const d = agentDisplay(a);
|
|
470
|
+
return '<div class="row"><div class="time">—</div><div class="body"><strong>' +
|
|
471
|
+
esc(d.client) + '</strong><span>' + esc(String(d.type).toUpperCase()) +
|
|
472
|
+
' · ' + esc(String(a.count)) + ' transactions</span></div><div></div></div>';
|
|
473
|
+
}).join("") + '</div>';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function renderPolicy(el) {
|
|
477
|
+
const { policy } = await api("/v1/policy");
|
|
478
|
+
const rules = (policy.rules || []).slice().sort(function (a, b) {
|
|
479
|
+
return (a.priority || 0) - (b.priority || 0);
|
|
480
|
+
});
|
|
481
|
+
let html = '<div class="panel"><h2>POLICY</h2>';
|
|
482
|
+
html += '<div class="rule"><div class="effect ' + esc(policy.defaultEffect || "ALLOW") + '">' +
|
|
483
|
+
esc(policy.defaultEffect || "ALLOW") + ' (default)</div>' +
|
|
484
|
+
'<div class="desc">When no rule matches, this decision applies.</div></div>';
|
|
485
|
+
if (!rules.length) {
|
|
486
|
+
html += '<div class="empty">No explicit rules.</div>';
|
|
487
|
+
} else {
|
|
488
|
+
html += rules.map(function (rule) {
|
|
489
|
+
const off = rule.enabled === false ? " off" : "";
|
|
490
|
+
return '<div class="rule' + off + '"><div class="effect ' + esc(rule.effect) + '">' +
|
|
491
|
+
esc(rule.effect) +
|
|
492
|
+
(rule.enabled === false ? " · DISABLED" : "") +
|
|
493
|
+
'</div><div class="desc">' + esc(rule.reason || rule.id || "Rule") + '</div>' +
|
|
494
|
+
'<div class="when">' + esc(ruleWhen(rule)) +
|
|
495
|
+
' · priority ' + esc(String(rule.priority)) + '</div></div>';
|
|
496
|
+
}).join("");
|
|
497
|
+
}
|
|
498
|
+
html += '</div>';
|
|
499
|
+
el.innerHTML = html;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function renderSettings(el) {
|
|
503
|
+
const status = statusCache || await api("/v1/status");
|
|
504
|
+
let accountHtml = "";
|
|
505
|
+
let accountNote = "";
|
|
506
|
+
try {
|
|
507
|
+
const account = await api("/v1/account");
|
|
508
|
+
if (account && account.email) {
|
|
509
|
+
accountHtml = '<dt>Account</dt><dd>' + esc(account.email) + '</dd>';
|
|
510
|
+
} else {
|
|
511
|
+
accountHtml = '<dt>Account</dt><dd>Not signed in</dd>';
|
|
512
|
+
accountNote = '<p class="note">Sign in with the REWIND CLI (login) to attach an account email here.</p>';
|
|
513
|
+
}
|
|
514
|
+
} catch (e) {
|
|
515
|
+
if (e.status === 404) {
|
|
516
|
+
accountHtml = '<dt>Account</dt><dd>—</dd>';
|
|
517
|
+
accountNote = '<p class="note">Account endpoint unavailable. Entitlement shown from status. Sign in via CLI.</p>';
|
|
518
|
+
} else {
|
|
519
|
+
accountHtml = '<dt>Account</dt><dd>—</dd>';
|
|
520
|
+
accountNote = '<p class="note">Could not load account. Sign in via CLI if needed.</p>';
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const ent = (status.entitlement && status.entitlement.label) || "—";
|
|
524
|
+
el.innerHTML =
|
|
525
|
+
'<div class="panel"><h2>SETTINGS</h2>' +
|
|
526
|
+
'<dl class="dl">' +
|
|
527
|
+
'<dt>Entitlement</dt><dd>' + esc(ent) + '</dd>' +
|
|
528
|
+
accountHtml +
|
|
529
|
+
'<dt>Project</dt><dd>' + esc(status.projectRoot || "—") + '</dd>' +
|
|
530
|
+
'<dt>Bind</dt><dd>127.0.0.1 (loopback only)</dd>' +
|
|
531
|
+
'</dl>' + accountNote +
|
|
532
|
+
'<p class="note">Raw status</p>' +
|
|
533
|
+
'<pre>' + esc(JSON.stringify({
|
|
534
|
+
projectRoot: status.projectRoot,
|
|
535
|
+
entitlement: status.entitlement,
|
|
536
|
+
protected: status.protected,
|
|
537
|
+
}, null, 2)) + '</pre></div>';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function render() {
|
|
541
|
+
document.querySelectorAll("nav button").forEach(function (b) {
|
|
542
|
+
b.classList.toggle("active", b.dataset.view === view);
|
|
543
|
+
});
|
|
544
|
+
const el = document.getElementById("view");
|
|
545
|
+
document.getElementById("flash").innerHTML = lastFlash;
|
|
546
|
+
try {
|
|
547
|
+
statusCache = await api("/v1/status");
|
|
548
|
+
document.getElementById("project").textContent = statusCache.projectRoot || "local project";
|
|
549
|
+
const conn = document.getElementById("conn");
|
|
550
|
+
conn.classList.remove("offline");
|
|
551
|
+
conn.textContent = "CONNECTED — mediated actions recorded";
|
|
552
|
+
if (view === "activity") await renderActivity(el);
|
|
553
|
+
else if (view === "approvals") await renderApprovals(el);
|
|
554
|
+
else if (view === "agents") await renderAgents(el);
|
|
555
|
+
else if (view === "policy") await renderPolicy(el);
|
|
556
|
+
else if (view === "settings") await renderSettings(el);
|
|
557
|
+
} catch (e) {
|
|
558
|
+
const conn = document.getElementById("conn");
|
|
559
|
+
conn.classList.add("offline");
|
|
560
|
+
conn.textContent = "DISCONNECTED";
|
|
561
|
+
el.innerHTML = '<div class="empty">API error: ' + esc(e.message) +
|
|
562
|
+
'. Start the control app with <code>rewind open</code> (opens with ?token=…).</div>';
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
document.querySelectorAll("nav button").forEach(function (b) {
|
|
567
|
+
b.onclick = function () {
|
|
568
|
+
view = b.dataset.view;
|
|
569
|
+
if (view !== "activity") selectedId = null;
|
|
570
|
+
render();
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
render();
|
|
574
|
+
setInterval(function () {
|
|
575
|
+
if (document.visibilityState === "visible") render();
|
|
576
|
+
}, 4000);
|
|
577
|
+
</script>
|
|
578
|
+
</body>
|
|
579
|
+
</html>`;
|
|
580
|
+
}
|
|
581
|
+
function checkoutResultHtml(kind) {
|
|
582
|
+
const title = kind === "subscribed" ? "Subscription received" : "Checkout canceled";
|
|
583
|
+
const body = kind === "subscribed"
|
|
584
|
+
? "Stripe TEST checkout completed. Personal activates after the signed webhook. Run <code>rewind status</code> or reopen the control app with <code>rewind open</code>."
|
|
585
|
+
: "Checkout was canceled. No charge was made. You can run <code>rewind subscribe</code> again when ready.";
|
|
586
|
+
return `<!DOCTYPE html>
|
|
587
|
+
<html lang="en"><head><meta charset="utf-8"/><title>REWIND — ${title}</title>
|
|
588
|
+
<style>
|
|
589
|
+
body{font-family:system-ui;background:#141210;color:#f2ebe3;min-height:100vh;display:flex;align-items:center;justify-content:center;margin:0}
|
|
590
|
+
main{max-width:28rem;padding:2rem;border:1px solid #2c2824;background:#1a1713}
|
|
591
|
+
h1{letter-spacing:.12em;font-size:1.1rem}p{color:#9a9086;line-height:1.5}code{color:#c4a574}
|
|
592
|
+
a{color:#c4a574}
|
|
593
|
+
</style></head><body><main>
|
|
594
|
+
<h1>REWIND</h1>
|
|
595
|
+
<p><strong>${title}</strong></p>
|
|
596
|
+
<p>${body}</p>
|
|
597
|
+
<p><a href="/">Open control app</a> (requires a session started via <code>rewind open</code>)</p>
|
|
598
|
+
</main></body></html>`;
|
|
599
|
+
}
|
|
600
|
+
export async function startLocalApiServer(options = {}) {
|
|
601
|
+
loadRewindEnv();
|
|
602
|
+
const projectRoot = options.projectRoot ?? getProjectRoot();
|
|
603
|
+
const port = options.port ?? 7429;
|
|
604
|
+
const host = "127.0.0.1";
|
|
605
|
+
const token = randomBytes(24).toString("hex");
|
|
606
|
+
// Token lives in the *project* .rewind/ — never under package dist/
|
|
607
|
+
const tokenPath = path.join(getRewindHome(projectRoot), "api-token");
|
|
608
|
+
await mkdir(path.dirname(tokenPath), { recursive: true });
|
|
609
|
+
await writeFile(tokenPath, token + "\n", { mode: 0o600 });
|
|
610
|
+
const auth = (req) => {
|
|
611
|
+
const h = req.headers.authorization ?? "";
|
|
612
|
+
if (h === `Bearer ${token}`)
|
|
613
|
+
return true;
|
|
614
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
615
|
+
if (url.searchParams.get("token") === token)
|
|
616
|
+
return true;
|
|
617
|
+
return false;
|
|
618
|
+
};
|
|
619
|
+
const server = http.createServer(async (req, res) => {
|
|
620
|
+
try {
|
|
621
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
622
|
+
const pathname = url.pathname;
|
|
623
|
+
// Public HTML — no bearer token (Stripe redirect must not strip auth awkwardly)
|
|
624
|
+
if (pathname === "/subscribed") {
|
|
625
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
626
|
+
res.end(checkoutResultHtml("subscribed"));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (pathname === "/canceled") {
|
|
630
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
631
|
+
res.end(checkoutResultHtml("canceled"));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// UI shell is public; API calls still require token (localStorage from ?token=)
|
|
635
|
+
if (pathname === "/" || pathname === "/app") {
|
|
636
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
637
|
+
res.end(controlAppHtml());
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (pathname === "/v1/health") {
|
|
641
|
+
json(res, 200, { ok: true, bind: `${host}:${port}` });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
// Stripe webhooks: raw body + signature (no local API bearer token).
|
|
645
|
+
// Loopback-only bind is the network boundary for this local proof.
|
|
646
|
+
if (pathname === "/v1/commercial/stripe/webhook" &&
|
|
647
|
+
req.method === "POST") {
|
|
648
|
+
const rawBody = await readBody(req);
|
|
649
|
+
const sig = typeof req.headers["stripe-signature"] === "string"
|
|
650
|
+
? req.headers["stripe-signature"]
|
|
651
|
+
: undefined;
|
|
652
|
+
const result = await handleStripeWebhookHttp(rawBody, sig);
|
|
653
|
+
json(res, result.httpStatus, result.body);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (!auth(req)) {
|
|
657
|
+
json(res, 401, { error: "Unauthorized — local token required" });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (pathname === "/v1/status" && req.method === "GET") {
|
|
661
|
+
const provider = await resolveEntitlementProvider();
|
|
662
|
+
setEntitlementProvider(provider);
|
|
663
|
+
const entitlement = await provider.check();
|
|
664
|
+
const protect = await loadProtectState(projectRoot).catch(() => null);
|
|
665
|
+
json(res, 200, {
|
|
666
|
+
projectRoot,
|
|
667
|
+
text: await cliStatus(projectRoot),
|
|
668
|
+
entitlement,
|
|
669
|
+
protected: Boolean(protect?.protected),
|
|
670
|
+
});
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (pathname === "/v1/transactions" && req.method === "GET") {
|
|
674
|
+
const limit = Number(url.searchParams.get("limit") ?? "50");
|
|
675
|
+
const transactions = await listTransactions(projectRoot, {
|
|
676
|
+
limit: Number.isFinite(limit) ? limit : 50,
|
|
677
|
+
});
|
|
678
|
+
json(res, 200, { transactions });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const txnMatch = pathname.match(/^\/v1\/transactions\/([^/]+)$/);
|
|
682
|
+
if (txnMatch && req.method === "GET") {
|
|
683
|
+
const txn = await getTransaction(txnMatch[1], projectRoot);
|
|
684
|
+
json(res, 200, { transaction: txn, text: formatTransaction(txn) });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const undoMatch = pathname.match(/^\/v1\/transactions\/([^/]+)\/undo$/);
|
|
688
|
+
if (undoMatch && req.method === "POST") {
|
|
689
|
+
const result = await undoTransaction(undoMatch[1], projectRoot);
|
|
690
|
+
json(res, result.ok ? 200 : result.conflict ? 409 : 400, result);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (pathname === "/v1/approvals" && req.method === "GET") {
|
|
694
|
+
json(res, 200, {
|
|
695
|
+
approvals: await listPendingApprovals(projectRoot),
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const apprMatch = pathname.match(/^\/v1\/approvals\/([^/]+)\/(approve|deny)$/);
|
|
700
|
+
if (apprMatch && req.method === "POST") {
|
|
701
|
+
const id = apprMatch[1];
|
|
702
|
+
const action = apprMatch[2];
|
|
703
|
+
const txn = action === "approve"
|
|
704
|
+
? await approvePending(id, projectRoot)
|
|
705
|
+
: await denyPending(id, projectRoot);
|
|
706
|
+
json(res, 200, { transaction: txn });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (pathname === "/v1/actors" && req.method === "GET") {
|
|
710
|
+
const txns = await listTransactions(projectRoot);
|
|
711
|
+
const map = new Map();
|
|
712
|
+
for (const t of txns) {
|
|
713
|
+
const key = `${t.actor.client}@${t.actor.clientVersion ?? ""}`;
|
|
714
|
+
const cur = map.get(key) ?? {
|
|
715
|
+
client: `${t.actor.client}@${t.actor.clientVersion ?? "?"}`,
|
|
716
|
+
type: t.actor.type,
|
|
717
|
+
count: 0,
|
|
718
|
+
};
|
|
719
|
+
cur.count++;
|
|
720
|
+
map.set(key, cur);
|
|
721
|
+
}
|
|
722
|
+
json(res, 200, { actors: [...map.values()] });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (pathname === "/v1/policy" && req.method === "GET") {
|
|
726
|
+
json(res, 200, { policy: await loadPolicy(projectRoot) });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (pathname === "/v1/account" && req.method === "GET") {
|
|
730
|
+
const session = await loadSession();
|
|
731
|
+
json(res, 200, {
|
|
732
|
+
email: session?.email ?? null,
|
|
733
|
+
signedIn: Boolean(session?.email),
|
|
734
|
+
});
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (pathname === "/v1/entitlement" && req.method === "GET") {
|
|
738
|
+
const provider = await resolveEntitlementProvider();
|
|
739
|
+
setEntitlementProvider(provider);
|
|
740
|
+
json(res, 200, {
|
|
741
|
+
entitlement: await provider.check(),
|
|
742
|
+
});
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
await readBody(req); // drain
|
|
746
|
+
json(res, 404, { error: `Not found: ${pathname}` });
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
const status = err instanceof TransactionEngineError && err.code === "CONFLICT"
|
|
750
|
+
? 409
|
|
751
|
+
: 500;
|
|
752
|
+
json(res, status, {
|
|
753
|
+
error: err instanceof Error ? err.message : String(err),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
await new Promise((resolve, reject) => {
|
|
758
|
+
server.once("error", reject);
|
|
759
|
+
server.listen(port, host, () => resolve());
|
|
760
|
+
});
|
|
761
|
+
const addr = server.address();
|
|
762
|
+
const boundPort = addr && typeof addr === "object" ? addr.port : port;
|
|
763
|
+
const url = `http://${host}:${boundPort}/?token=${token}`;
|
|
764
|
+
console.log("REWIND LOCAL API");
|
|
765
|
+
console.log("");
|
|
766
|
+
console.log(`Bound ${host}:${boundPort} (loopback only)`);
|
|
767
|
+
console.log(`Token file ${tokenPath}`);
|
|
768
|
+
console.log(`Control UI ${url}`);
|
|
769
|
+
console.log("");
|
|
770
|
+
console.log("Not exposed on 0.0.0.0. Token required for mutations.");
|
|
771
|
+
if (options.listen !== false) {
|
|
772
|
+
await new Promise(() => {
|
|
773
|
+
/* run until killed */
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
port: boundPort,
|
|
778
|
+
host,
|
|
779
|
+
token,
|
|
780
|
+
url,
|
|
781
|
+
close: () => new Promise((resolve, reject) => {
|
|
782
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
783
|
+
}),
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
//# sourceMappingURL=server.js.map
|