@startup-api/cloudflare 0.2.0 → 0.3.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 +28 -1
- package/package.json +1 -1
- package/public/users/accounts.html +4 -7
- package/public/users/admin/index.html +73 -19
- package/public/users/power-strip.js +279 -42
- package/public/users/profile.html +5 -8
- package/public/users/style.css +149 -59
- package/src/PowerStrip.ts +1 -1
- package/src/createStartupAPI.ts +48 -5
- package/src/policy/accessPolicy.ts +4 -3
- package/src/schemas/policy.ts +25 -1
package/README.md
CHANGED
|
@@ -160,7 +160,34 @@ Configure an ordered list of rules (first match wins) mapping a path pattern to
|
|
|
160
160
|
- **`authenticated`** — any logged-in user.
|
|
161
161
|
- **`entitlement`** — a provider condition: Patreon `active_patron`, a specific `benefit` (perk) ID, or a `tier` ID.
|
|
162
162
|
|
|
163
|
-
Patterns are exact (`/special`), prefix (`/app/*`), or `/` (homepage only). Each rule's `on_unauthorized` is `login` (redirect to sign in), `forbidden` (403),
|
|
163
|
+
Patterns are exact (`/special`), prefix (`/app/*`), or `/` (homepage only). Each rule's `on_unauthorized` is `login` (redirect to sign in), `forbidden` (403), `upgrade` (redirect to `upgrade_url`, e.g. a Patreon join page), or `gate` (serve an explainer page **in place**, with no redirect — see below). When no policy is configured at all, every path is treated as `public` (backward compatible).
|
|
164
|
+
|
|
165
|
+
#### Serving a gate page in place (`on_unauthorized: 'gate'`)
|
|
166
|
+
|
|
167
|
+
Instead of redirecting, a denied request can serve an explainer page **at the requested URL** (no redirect, status `200` by default). The page shown depends on login state, so anonymous and logged-in-but-unentitled visitors can see different copy:
|
|
168
|
+
|
|
169
|
+
- **`anonymous`** (required) — shown to visitors who are **not** logged in (e.g. a "become a patron + log in" page).
|
|
170
|
+
- **`unentitled`** (optional) — shown to logged-in visitors who fail the requirement (e.g. a "pledge/upgrade" page). Falls back to `anonymous` when omitted.
|
|
171
|
+
- **`status`** (optional) — HTTP status for the served page; defaults to `200` to preserve typical explainer-page UX (set `403` if you prefer).
|
|
172
|
+
|
|
173
|
+
Each variant is a `PageSource` whose body comes from **either** the `ASSETS` binding **or** a path proxied from `ORIGIN_URL` — exactly one of:
|
|
174
|
+
|
|
175
|
+
- **`{ asset: '/early-access' }`** — a local file from `ASSETS` (resolved like other assets, `/early-access` → `early-access.html`).
|
|
176
|
+
- **`{ origin: '/early-access' }`** — a path proxied from `ORIGIN_URL`. The path must be reachable directly on the origin (the raw site, not behind this worker).
|
|
177
|
+
|
|
178
|
+
The gate config is set per rule via `gate`, or on the policy default via `default_gate`. The served gate page is produced inside the deny path, so it is not re-subjected to the access policy and no power-strip is injected — the page is expected to carry its own login CTA. Because nothing redirects, there is no loop risk.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const accessPolicy = {
|
|
182
|
+
default: { mode: 'entitlement', provider: 'patreon', condition: { type: 'benefit', benefit_id: '<BENEFIT_ID>' } },
|
|
183
|
+
default_on_unauthorized: 'gate',
|
|
184
|
+
default_gate: {
|
|
185
|
+
anonymous: { origin: '/early-access' }, // or { asset: '/early-access' }
|
|
186
|
+
unentitled: { origin: '/pledge-needed' }, // or { asset: '/pledge-needed' }
|
|
187
|
+
// status: 403, // optional; defaults to 200
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
```
|
|
164
191
|
|
|
165
192
|
Admin users (those listed in `ADMIN_IDS`) bypass every `authenticated`/`entitlement` requirement and can reach any gated path. Their identity is still resolved and the usual identity/entitlement headers are forwarded to the origin — only the gate itself is skipped. (`bypass` paths remain a raw pass-through for everyone, with no identity resolution.)
|
|
166
193
|
|
package/package.json
CHANGED
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
data-ssr-members="{{ssr:account_members_json}}"
|
|
13
13
|
data-ssr-plans="{{ssr:plans_json}}"
|
|
14
14
|
>
|
|
15
|
-
<power-strip
|
|
16
|
-
providers="{{ssr:providers}}"
|
|
17
|
-
style="position: absolute; top: 0; right: 0; z-index: 9999; padding: 0.1rem; border-radius: 0 0 0 0.3rem"
|
|
18
|
-
>
|
|
15
|
+
<power-strip providers="{{ssr:providers}}" style="position: absolute; top: 0; right: 0; z-index: 9999; border-radius: 0 0 0 0.3rem">
|
|
19
16
|
<svg viewBox="0 0 24 24" style="width: 1rem; height: 1rem"><path d="M7 2v11h3v9l7-12h-4l4-8z" fill="#ffcc00" /></svg>
|
|
20
17
|
</power-strip>
|
|
21
18
|
<script src="/users/power-strip.js" async></script>
|
|
@@ -61,7 +58,7 @@
|
|
|
61
58
|
<div
|
|
62
59
|
id="account-avatar-placeholder"
|
|
63
60
|
class="account-avatar-large"
|
|
64
|
-
style="background:
|
|
61
|
+
style="background: var(--surface-alt); {{ssr:account_placeholder_display}} align-items: center; justify-content: center; color: var(--muted-badge-text)"
|
|
65
62
|
>
|
|
66
63
|
<svg viewBox="0 0 24 24" style="width: 48px; height: 48px; fill: currentColor">
|
|
67
64
|
<path
|
|
@@ -97,8 +94,8 @@
|
|
|
97
94
|
</button>
|
|
98
95
|
</div>
|
|
99
96
|
<div>
|
|
100
|
-
<h2 id="display-account-name" style="margin: 0; color:
|
|
101
|
-
<p id="display-account-plan" style="margin: 0.25rem 0 0 0; color:
|
|
97
|
+
<h2 id="display-account-name" style="margin: 0; color: var(--text)">{{ssr:account_name}}</h2>
|
|
98
|
+
<p id="display-account-plan" style="margin: 0.25rem 0 0 0; color: var(--text-faint)">{{ssr:account_plan_name}}</p>
|
|
102
99
|
</div>
|
|
103
100
|
</div>
|
|
104
101
|
|
|
@@ -5,6 +5,56 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Admin Dashboard</title>
|
|
7
7
|
<style>
|
|
8
|
+
/* Color tokens, flipped by the user's OS color-scheme preference (with a
|
|
9
|
+
[data-theme] escape hatch) — same approach as the shared user pages
|
|
10
|
+
(/users/style.css) and the landing page. */
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #f9f9f9;
|
|
13
|
+
--surface: #fff;
|
|
14
|
+
--surface-muted: #eee;
|
|
15
|
+
--hover-bg: #f0f0f0;
|
|
16
|
+
--text: #333;
|
|
17
|
+
--border: #ddd;
|
|
18
|
+
--border-light: #eee;
|
|
19
|
+
--accent: #ffcc00;
|
|
20
|
+
--accent-hover: #e6b800;
|
|
21
|
+
--on-accent: #202124;
|
|
22
|
+
--secondary-btn-bg: #eee;
|
|
23
|
+
--secondary-btn-hover-bg: #e0e0e0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@media (prefers-color-scheme: dark) {
|
|
27
|
+
:root:not([data-theme='light']) {
|
|
28
|
+
--bg: #1a1a1a;
|
|
29
|
+
--surface: #2d2d2d;
|
|
30
|
+
--surface-muted: #333;
|
|
31
|
+
--hover-bg: #3c4043;
|
|
32
|
+
--text: #e0e0e0;
|
|
33
|
+
--border: #5f6368;
|
|
34
|
+
--border-light: #444;
|
|
35
|
+
--accent: #ffcc00;
|
|
36
|
+
--accent-hover: #e6b800;
|
|
37
|
+
--on-accent: #202124;
|
|
38
|
+
--secondary-btn-bg: #3c4043;
|
|
39
|
+
--secondary-btn-hover-bg: #4a4f54;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[data-theme='dark'] {
|
|
44
|
+
--bg: #1a1a1a;
|
|
45
|
+
--surface: #2d2d2d;
|
|
46
|
+
--surface-muted: #333;
|
|
47
|
+
--hover-bg: #3c4043;
|
|
48
|
+
--text: #e0e0e0;
|
|
49
|
+
--border: #5f6368;
|
|
50
|
+
--border-light: #444;
|
|
51
|
+
--accent: #ffcc00;
|
|
52
|
+
--accent-hover: #e6b800;
|
|
53
|
+
--on-accent: #202124;
|
|
54
|
+
--secondary-btn-bg: #3c4043;
|
|
55
|
+
--secondary-btn-hover-bg: #4a4f54;
|
|
56
|
+
}
|
|
57
|
+
|
|
8
58
|
body {
|
|
9
59
|
font-family:
|
|
10
60
|
system-ui,
|
|
@@ -13,14 +63,15 @@
|
|
|
13
63
|
padding: 2rem;
|
|
14
64
|
max-width: 1200px;
|
|
15
65
|
margin: 0 auto;
|
|
16
|
-
background:
|
|
66
|
+
background: var(--bg);
|
|
67
|
+
color: var(--text);
|
|
17
68
|
}
|
|
18
69
|
h1,
|
|
19
70
|
h2 {
|
|
20
|
-
color:
|
|
71
|
+
color: var(--text);
|
|
21
72
|
}
|
|
22
73
|
section {
|
|
23
|
-
background:
|
|
74
|
+
background: var(--surface);
|
|
24
75
|
padding: 1.5rem;
|
|
25
76
|
border-radius: 8px;
|
|
26
77
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
@@ -33,20 +84,22 @@
|
|
|
33
84
|
}
|
|
34
85
|
input[type='text'] {
|
|
35
86
|
padding: 0.5rem;
|
|
36
|
-
border: 1px solid
|
|
87
|
+
border: 1px solid var(--border);
|
|
37
88
|
border-radius: 4px;
|
|
38
89
|
flex-grow: 1;
|
|
90
|
+
background: var(--surface);
|
|
91
|
+
color: var(--text);
|
|
39
92
|
}
|
|
40
93
|
button {
|
|
41
94
|
padding: 0.5rem 1rem;
|
|
42
|
-
background:
|
|
43
|
-
color:
|
|
95
|
+
background: var(--accent);
|
|
96
|
+
color: var(--on-accent);
|
|
44
97
|
border: none;
|
|
45
98
|
border-radius: 4px;
|
|
46
99
|
cursor: pointer;
|
|
47
100
|
}
|
|
48
101
|
button:hover {
|
|
49
|
-
background:
|
|
102
|
+
background: var(--accent-hover);
|
|
50
103
|
}
|
|
51
104
|
table {
|
|
52
105
|
width: 100%;
|
|
@@ -56,21 +109,21 @@
|
|
|
56
109
|
td {
|
|
57
110
|
text-align: left;
|
|
58
111
|
padding: 0.75rem;
|
|
59
|
-
border-bottom: 1px solid
|
|
112
|
+
border-bottom: 1px solid var(--border-light);
|
|
60
113
|
}
|
|
61
114
|
th {
|
|
62
|
-
background:
|
|
115
|
+
background: var(--hover-bg);
|
|
63
116
|
}
|
|
64
117
|
.actions {
|
|
65
118
|
display: flex;
|
|
66
119
|
gap: 0.5rem;
|
|
67
120
|
}
|
|
68
121
|
.secondary-btn {
|
|
69
|
-
background:
|
|
70
|
-
color:
|
|
122
|
+
background: var(--secondary-btn-bg);
|
|
123
|
+
color: var(--text);
|
|
71
124
|
}
|
|
72
125
|
.secondary-btn:hover {
|
|
73
|
-
background:
|
|
126
|
+
background: var(--secondary-btn-hover-bg);
|
|
74
127
|
}
|
|
75
128
|
#toast {
|
|
76
129
|
position: fixed;
|
|
@@ -89,6 +142,8 @@
|
|
|
89
142
|
border-radius: 8px;
|
|
90
143
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
91
144
|
width: 400px;
|
|
145
|
+
background: var(--surface);
|
|
146
|
+
color: var(--text);
|
|
92
147
|
}
|
|
93
148
|
dialog::backdrop {
|
|
94
149
|
background: rgba(0, 0, 0, 0.5);
|
|
@@ -108,9 +163,11 @@
|
|
|
108
163
|
.form-group select {
|
|
109
164
|
width: 100%;
|
|
110
165
|
padding: 0.5rem;
|
|
111
|
-
border: 1px solid
|
|
166
|
+
border: 1px solid var(--border);
|
|
112
167
|
border-radius: 4px;
|
|
113
168
|
box-sizing: border-box;
|
|
169
|
+
background: var(--surface);
|
|
170
|
+
color: var(--text);
|
|
114
171
|
}
|
|
115
172
|
.modal-actions {
|
|
116
173
|
display: flex;
|
|
@@ -121,10 +178,7 @@
|
|
|
121
178
|
</style>
|
|
122
179
|
</head>
|
|
123
180
|
<body data-ssr-plans="{{ssr:plans_json}}">
|
|
124
|
-
<power-strip
|
|
125
|
-
providers="{{ssr:providers}}"
|
|
126
|
-
style="position: absolute; top: 0; right: 0; z-index: 9999; padding: 0.1rem; border-radius: 0 0 0 0.3rem"
|
|
127
|
-
>
|
|
181
|
+
<power-strip providers="{{ssr:providers}}" style="position: absolute; top: 0; right: 0; z-index: 9999; border-radius: 0 0 0 0.3rem">
|
|
128
182
|
<svg viewBox="0 0 24 24" style="width: 1rem; height: 1rem"><path d="M7 2v11h3v9l7-12h-4l4-8z" fill="#ffcc00" /></svg>
|
|
129
183
|
</power-strip>
|
|
130
184
|
<script src="/users/power-strip.js" async></script>
|
|
@@ -417,7 +471,7 @@
|
|
|
417
471
|
<button class="secondary-btn" onclick="openEditUser('${u.id}')">Edit</button>
|
|
418
472
|
<button class="secondary-btn" onclick="openUserMemberships('${u.id}')">Memberships</button>
|
|
419
473
|
<button onclick="impersonate('${u.id}')">Impersonate</button>
|
|
420
|
-
<button style="background: #d93025;" onclick="deleteUser('${u.id}')">Delete</button>
|
|
474
|
+
<button style="background: #d93025; color: #fff;" onclick="deleteUser('${u.id}')">Delete</button>
|
|
421
475
|
</td>
|
|
422
476
|
</tr>
|
|
423
477
|
`,
|
|
@@ -443,7 +497,7 @@
|
|
|
443
497
|
<td class="actions">
|
|
444
498
|
<button class="secondary-btn" onclick="openEditAccount('${a.id}')">Edit</button>
|
|
445
499
|
<button class="secondary-btn" onclick="openMembers('${a.id}')">Members</button>
|
|
446
|
-
<button style="background: #d93025;" onclick="deleteAccount('${a.id}')">Delete</button>
|
|
500
|
+
<button style="background: #d93025; color: #fff;" onclick="deleteAccount('${a.id}')">Delete</button>
|
|
447
501
|
</td>
|
|
448
502
|
</tr>
|
|
449
503
|
`;
|