@dcl-regenesislabs/artifacts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +357 -0
- package/cli/artifacts.mjs +706 -0
- package/package.json +48 -0
- package/shared/artifact-id.js +175 -0
- package/templates/artifact/assets/dcl.css +279 -0
- package/templates/artifact/assets/dcl.js +62 -0
- package/templates/artifact/index.html +161 -0
package/README.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# ia-generated-artifacts
|
|
2
|
+
|
|
3
|
+
Self-hosted artifact host — a Claude-Artifacts-like place to drop a folder of static
|
|
4
|
+
files and get a link back. Runs as a Cloudflare Worker in front of an R2 bucket on
|
|
5
|
+
the **dclregenesislabs** account.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
https://artifacts.dclregenesislabs.xyz/{id} latest version
|
|
9
|
+
https://artifacts.dclregenesislabs.xyz/{id}@2 one version, pinned
|
|
10
|
+
https://artifacts.dclregenesislabs.xyz/p/{alias} public link, no login
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`id` is a UUID v4 minted on the first push and kept for life. Every later push to
|
|
14
|
+
the same id is a new **version**; the link never changes. Every gated page gets a
|
|
15
|
+
**bar** at the top, injected by the Worker: the artifact's name, a version picker,
|
|
16
|
+
the **public link switch**, copy link. Checkboxes marked `data-persist` remember
|
|
17
|
+
their state on the host, for everyone.
|
|
18
|
+
|
|
19
|
+
Every `/{id}` URL is behind a **Cloudflare Access** login that only accepts
|
|
20
|
+
`@dclregenesislabs.xyz` and `@decentraland.org` addresses. Making something
|
|
21
|
+
public is done from the bar, by a signed-in person — never by the upload token.
|
|
22
|
+
See [Visibility](#visibility).
|
|
23
|
+
|
|
24
|
+
## Publish something
|
|
25
|
+
|
|
26
|
+
The CLI is on npm as [`@dcl-regenesislabs/artifacts`](https://www.npmjs.com/package/@dcl-regenesislabs/artifacts),
|
|
27
|
+
so nothing needs installing:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx @dcl-regenesislabs/artifacts login # once per machine, work email in a browser
|
|
31
|
+
npx @dcl-regenesislabs/artifacts push ./my-demo --name "my demo"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
From a checkout of this repo, the same commands are `node cli/artifacts.mjs …`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
node cli/artifacts.mjs login
|
|
38
|
+
node cli/artifacts.mjs push ./my-demo --name "my demo"
|
|
39
|
+
# → https://artifacts.dclregenesislabs.xyz/3f2b9c1e-4d5a-4b6c-8e7f-9a0b1c2d3e4f
|
|
40
|
+
|
|
41
|
+
node cli/artifacts.mjs push ./my-demo --id https://artifacts.dclregenesislabs.xyz/3f2b9c1e-… # new version, same link
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`index.html` is the entry point. A single file is served at the bare `/{id}`. A
|
|
45
|
+
folder is served under `/{id}/` (the bare URL redirects there), so reference its
|
|
46
|
+
assets **relatively** (`./assets/app.js`) — absolute paths break.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node cli/artifacts.mjs push ./report.html # single-file artifact
|
|
50
|
+
node cli/artifacts.mjs push ./demos/*/ # several new artifacts at once
|
|
51
|
+
node cli/artifacts.mjs info <id or url> # name, versions, visibility
|
|
52
|
+
node cli/artifacts.mjs ls # everything on the host
|
|
53
|
+
node cli/artifacts.mjs rm <id or url> # every version, state and public link
|
|
54
|
+
node cli/artifacts.mjs --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Pushing bytes identical to the latest version records nothing. Blobs are stored
|
|
58
|
+
by digest, so a new version uploads only the files that changed. `commit` carries
|
|
59
|
+
the version number the client last saw and is refused if someone pushed in between.
|
|
60
|
+
|
|
61
|
+
## House template
|
|
62
|
+
|
|
63
|
+
`templates/artifact/` is the look every page on the host shares — IBM Plex,
|
|
64
|
+
indigo accent, three-state dark mode, and a set of blocks (callouts, cards,
|
|
65
|
+
tables, phases, checklist, diff columns). It was lifted from a Claude-designed
|
|
66
|
+
artifact so pages here match the ones Claude makes elsewhere.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
node cli/artifacts.mjs new ./my-page --name "Q3 numbers" # scaffold
|
|
70
|
+
$EDITOR my-page/index.html # write
|
|
71
|
+
node cli/artifacts.mjs push ./my-page # publish
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`new` writes **one self-contained `index.html`**: the CSS and JS in
|
|
75
|
+
`templates/artifact/assets/` stay editable as files and are inlined at
|
|
76
|
+
scaffold time, so the result is the same single-file shape as a Claude artifact
|
|
77
|
+
and works anywhere. The scaffolded page is a live reference of every block. The
|
|
78
|
+
one knob per page is `--accent` in its small `<style>`. The skill tells Claude
|
|
79
|
+
to start here. (`docs/guide/index.html` is generated by `scripts/build-guide.mjs`
|
|
80
|
+
from the same assets — re-run it when `dcl.css` changes. The guide carries the
|
|
81
|
+
install line with the token filled in, so it stays team-only: never flip it
|
|
82
|
+
public.)
|
|
83
|
+
|
|
84
|
+
## The bar
|
|
85
|
+
|
|
86
|
+
The Worker injects `<dcl-bar>` plus `/__ui/bar.js` at the top of `<body>` of
|
|
87
|
+
every HTML response on a gated URL (`src/serve.ts`, HTMLRewriter). The script
|
|
88
|
+
renders into a shadow root, so page CSS can't touch it and vice versa. It talks
|
|
89
|
+
to a small in-page API under the artifact:
|
|
90
|
+
|
|
91
|
+
| Call | Who | Does |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `GET /{id}/__ui/meta` | anyone through the gate | name, versions, public link, viewer, checkbox state |
|
|
94
|
+
| `PUT /{id}/__ui/public` | a signed-in person | mints the public alias; records who and when |
|
|
95
|
+
| `DELETE /{id}/__ui/public` | a signed-in person | deletes the alias — the link dies at once |
|
|
96
|
+
| `PUT /{id}/__ui/state` | a signed-in person | `{ "key": true }` merges into the checkbox state |
|
|
97
|
+
| `GET /p/{alias}/__ui/state` | anyone with the link | the state, read-only |
|
|
98
|
+
|
|
99
|
+
"Signed-in person" means the Worker verified the Cloudflare Access JWT
|
|
100
|
+
(`src/access.ts`: signature against the team's public keys, audience, issuer,
|
|
101
|
+
expiry) and took the email from it. That is the answer to *who can make a
|
|
102
|
+
document public*: anyone who can open it. The public view (`/p/…`) gets no bar
|
|
103
|
+
and only a tiny script that paints the saved checkbox state.
|
|
104
|
+
|
|
105
|
+
**Checkboxes.** `<input type="checkbox" data-persist="deploy-staging">`. State is
|
|
106
|
+
keyed by artifact, not version, so ticks survive a re-push as long as the keys do.
|
|
107
|
+
The bar shows who ticked each box. Public viewers see them read-only.
|
|
108
|
+
|
|
109
|
+
## Visibility
|
|
110
|
+
|
|
111
|
+
| | URL | Who can open it |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| **team** (always) | `/{id}`, `/{id}@n` | `@dclregenesislabs.xyz` and `@decentraland.org` only, via a work-email login |
|
|
114
|
+
| **public** (switch in the bar) | `/p/{alias}` | anyone holding the link, no login; latest version; no bar |
|
|
115
|
+
|
|
116
|
+
The public link is additive — it mints a *second* URL and the gated one keeps
|
|
117
|
+
working. Everything under `/p/` is exempt from the Access policy, so **the alias
|
|
118
|
+
is the only credential**: 128 bits of randomness, unrelated to the artifact id.
|
|
119
|
+
|
|
120
|
+
Switching it off deletes the alias immediately — aliases are resolved against R2
|
|
121
|
+
on every request rather than cached — and it is permanent: switching on again
|
|
122
|
+
mints a *different* alias, so a link already sent out stays dead. Neither the CLI
|
|
123
|
+
nor the upload API can do any of this; there is no endpoint for it.
|
|
124
|
+
|
|
125
|
+
Claude picks all of this up through the project skill in
|
|
126
|
+
`.claude/skills/dcl-artifacts/`.
|
|
127
|
+
|
|
128
|
+
## Install the skill (one line)
|
|
129
|
+
|
|
130
|
+
macOS / Linux:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
curl -fsSL https://artifacts.dclregenesislabs.xyz/install.sh | bash
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Windows (PowerShell):
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
irm https://artifacts.dclregenesislabs.xyz/install.ps1 | iex
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
No secret in either line. `scripts/install.sh` / `scripts/install.ps1` are
|
|
143
|
+
served by the Worker at a public path together with `dist/dcl-artifacts.tgz` —
|
|
144
|
+
the CLI, the shared id module, the house template and the skill with its style
|
|
145
|
+
reference, built by `scripts/build-dist.mjs` before every `npm run dev` /
|
|
146
|
+
`npm run deploy` and checked to contain nothing token-shaped. The installer
|
|
147
|
+
unpacks the tarball into `~/.dcl-artifacts`, writes the skill to
|
|
148
|
+
`~/.claude/skills/dcl-artifacts/` with that path filled in, and then runs
|
|
149
|
+
`dcl-artifacts login`, which opens a browser for the work-email one-time PIN.
|
|
150
|
+
Needs Node 20+, nothing else — no git, no repo access, no `npm install`.
|
|
151
|
+
Re-run the same line to update.
|
|
152
|
+
|
|
153
|
+
By hand, with access to this repo (no `npm install` needed; run
|
|
154
|
+
`node cli/artifacts.mjs login` afterwards):
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
git clone git@github.com:dcl-regenesislabs/ia-generated-artifacts.git ~/.dcl-artifacts
|
|
158
|
+
~/.dcl-artifacts/scripts/install-skill.sh # or: npm run skill:install, inside the checkout
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The one-line installers leave an existing checkout at `~/.dcl-artifacts` alone,
|
|
162
|
+
so the two ways don't fight.
|
|
163
|
+
|
|
164
|
+
## Signing in
|
|
165
|
+
|
|
166
|
+
`dcl-artifacts login` binds a browser login to the terminal that asked for it:
|
|
167
|
+
|
|
168
|
+
1. The CLI holds a loopback port, then opens `/cli/login` with the port, a
|
|
169
|
+
random `state`, and `sha256(verifier)` — the verifier never leaves the process.
|
|
170
|
+
2. Cloudflare Access gates that page, so reaching it proves a work-email login.
|
|
171
|
+
The page shows a code that must match the terminal, and minting only happens
|
|
172
|
+
on a POST from that page.
|
|
173
|
+
3. The Worker mints a one-time code (60s, single use, R2) and redirects to
|
|
174
|
+
`http://127.0.0.1:<port>/cb` — a target it builds itself from a validated
|
|
175
|
+
integer, never from the query string.
|
|
176
|
+
4. The CLI checks `state`, then exchanges the code plus the verifier at
|
|
177
|
+
`/_api/cli/exchange` for a token, and writes it to
|
|
178
|
+
`~/.config/dcl-artifacts/credentials.json` (mode 600).
|
|
179
|
+
|
|
180
|
+
Tokens are `dcla_v1_…`, live 90 days, and are stored only as `sha256` in KV.
|
|
181
|
+
Revoke one from `/cli/tokens` or with `dcl-artifacts logout [--all]`. Nothing is
|
|
182
|
+
ever pasted anywhere, and the token is never printed.
|
|
183
|
+
|
|
184
|
+
## Layout
|
|
185
|
+
|
|
186
|
+
| Path | What it is |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `src/index.ts` | router: `/_api/*` → upload API, everything else → site |
|
|
189
|
+
| `src/api.ts` | `status` / `blobs` / `commit` / info / list / delete, all bearer-authed |
|
|
190
|
+
| `src/serve.ts` | reads: version resolution, `@n`, redirects, `index.html` + SPA fallback, ETag/Range/cache, bar injection |
|
|
191
|
+
| `src/ui.ts` | the in-page API the bar calls |
|
|
192
|
+
| `src/access.ts` | Cloudflare Access JWT verification |
|
|
193
|
+
| `src/bar-script.ts` | the injected client code (bar, and the public-view state painter) |
|
|
194
|
+
| `src/store.ts` | meta / state / manifest records, compare-and-swap on the R2 ETag |
|
|
195
|
+
| `src/auth.ts`, `src/mime.ts`, `src/types.ts`, `src/urls.ts` | token comparison, content types, bindings and record shapes, URL helpers |
|
|
196
|
+
| `shared/artifact-id.js` | id / alias / hash / manifest rules — imported by *both* the Worker and the CLI |
|
|
197
|
+
| `cli/artifacts.mjs` | zero-dependency Node uploader |
|
|
198
|
+
| `scripts/smoke.sh` | end-to-end check against a local or deployed host |
|
|
199
|
+
| `scripts/setup-access.mjs` | reconciles the Cloudflare Access apps and policies |
|
|
200
|
+
| `scripts/build-guide.mjs` | regenerates `docs/guide/index.html` from the template assets |
|
|
201
|
+
| `scripts/install.sh`, `scripts/install.ps1` | the one-line installers, served at `/install.sh` and `/install.ps1` |
|
|
202
|
+
| `scripts/build-dist.mjs` | builds `dist/dcl-artifacts.tgz`, served at `/install/dcl-artifacts.tgz` |
|
|
203
|
+
|
|
204
|
+
R2 layout, per artifact:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
a/{id}/__meta.json name, versions[], current, public {alias, at, by} (mutable, CAS)
|
|
208
|
+
a/{id}/__state.json checkbox state {key: {checked, by, at}} (mutable, CAS)
|
|
209
|
+
a/{id}/m/{n}.json file list of version n (immutable)
|
|
210
|
+
a/{id}/b/{sha256} blob, one per distinct content (immutable)
|
|
211
|
+
p/{alias} → {id} (exists only while public)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Nothing under `a/` is ever served by path — the Worker resolves a URL through the
|
|
215
|
+
meta record and the version's manifest to a blob. Blobs and manifests are cached
|
|
216
|
+
at the edge under keys the router 404s.
|
|
217
|
+
|
|
218
|
+
## How an upload works
|
|
219
|
+
|
|
220
|
+
The client hashes locally and uploads straight to blob keys, so the Worker never
|
|
221
|
+
streams objects through itself:
|
|
222
|
+
|
|
223
|
+
1. `POST /_api/artifacts/{id}/status` — sends the file list, gets back
|
|
224
|
+
`{exists, current, missing: [sha256…]}`.
|
|
225
|
+
2. `PUT /_api/artifacts/{id}/blobs/{sha256}` — raw bytes. The Worker checks the
|
|
226
|
+
digest and hands the same digest to R2, which checks it again.
|
|
227
|
+
3. `POST /_api/artifacts/{id}/commit` — `{files, base, name?, author?}`. The
|
|
228
|
+
Worker confirms every blob is present, writes the version manifest, then
|
|
229
|
+
updates the meta record — refusing if `current !== base`. Only then is the
|
|
230
|
+
version live.
|
|
231
|
+
|
|
232
|
+
All `/_api/*` calls require `Authorization: Bearer <token>` — either a per-user
|
|
233
|
+
token from `login` or, until it is retired, the shared `UPLOAD_TOKEN`. The two
|
|
234
|
+
exceptions are `POST /_api/cli/exchange`, which is how the CLI gets a token in
|
|
235
|
+
the first place (PKCE, not a bearer), and `GET /_api/whoami`, which reports what
|
|
236
|
+
the caller is signed in as.
|
|
237
|
+
|
|
238
|
+
`commit` records `by` from the token's own email; the client-supplied `author`
|
|
239
|
+
is honoured only for the shared token, which carries no identity.
|
|
240
|
+
|
|
241
|
+
## Who can read what
|
|
242
|
+
|
|
243
|
+
Cloudflare Access sits in front of the hostname. It matches the most specific
|
|
244
|
+
path first, which gives these rules:
|
|
245
|
+
|
|
246
|
+
| Path | Policy | Why |
|
|
247
|
+
| --- | --- | --- |
|
|
248
|
+
| `/_api/*` | bypass | already guarded by the bearer token |
|
|
249
|
+
| `/_health` | bypass | exposes nothing, keeps uptime monitors working |
|
|
250
|
+
| `/p/*` | bypass | opt-in public aliases carry their own secret |
|
|
251
|
+
| `/install.sh`, `/install.ps1`, `/install/*` | bypass | the installers and their tarball; no secrets in any of them |
|
|
252
|
+
| everything else | allow `@dclregenesislabs.xyz`, `@decentraland.org` | the gate — pages, `/__ui/bar.js`, the in-page API |
|
|
253
|
+
|
|
254
|
+
Login is Cloudflare's built-in **one-time PIN**: the visitor types their email,
|
|
255
|
+
gets a 6-digit code, and the policy checks the domain. No identity provider to
|
|
256
|
+
configure. Sessions last a week. A service-token policy also lets CI reach the
|
|
257
|
+
gated path — `scripts/setup-access.mjs` mints that token; the Worker attributes
|
|
258
|
+
its in-page writes to the token's name.
|
|
259
|
+
|
|
260
|
+
The Worker also verifies the Access JWT itself for in-page writes. `ACCESS_AUD`
|
|
261
|
+
in `wrangler.jsonc` is the AUD tag of the catch-all "Artifacts" application —
|
|
262
|
+
the `kid` in its login redirect. If that application is ever recreated, update it.
|
|
263
|
+
|
|
264
|
+
`workers_dev` and `preview_urls` are pinned to `false` in `wrangler.jsonc`. That
|
|
265
|
+
is load-bearing: a `*.workers.dev` URL would answer on a hostname Access does not
|
|
266
|
+
cover and would hand anyone a way straight past the gate.
|
|
267
|
+
|
|
268
|
+
## Develop
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npm install
|
|
272
|
+
printf 'UPLOAD_TOKEN=%s\nPUBLIC_BASE_URL=http://127.0.0.1:8787\nLOCAL_DEV=1\n' "$(openssl rand -hex 32)" > .dev.vars
|
|
273
|
+
npm run dev # wrangler dev, local R2
|
|
274
|
+
./scripts/smoke.sh http://127.0.0.1:8787 "$(sed -n 's/^UPLOAD_TOKEN=//p' .dev.vars)"
|
|
275
|
+
npm run typecheck
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`LOCAL_DEV=1` stands in for Access locally: in-page writes are attributed to
|
|
279
|
+
`dev@localhost`. It lives only in `.dev.vars`, which is gitignored and never
|
|
280
|
+
deployed.
|
|
281
|
+
|
|
282
|
+
## Deploy
|
|
283
|
+
|
|
284
|
+
Wrangler must be logged into the **dclregenesislabs** account (`wrangler whoami`
|
|
285
|
+
should show `a2b29bacd555c6fc78becaad8b183e9c`; `wrangler login` to switch).
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
npx wrangler r2 bucket create dcl-artifacts
|
|
289
|
+
npx wrangler r2 bucket create dcl-artifacts-preview # used by `wrangler dev --remote`
|
|
290
|
+
npx wrangler kv namespace create TOKENS # put the id in wrangler.jsonc
|
|
291
|
+
npx wrangler kv namespace create TOKENS --preview # and the preview_id
|
|
292
|
+
npx wrangler secret put UPLOAD_TOKEN # the shared token, until it is retired
|
|
293
|
+
npm run deploy # builds dist/, provisions the custom domain
|
|
294
|
+
|
|
295
|
+
# Cloudflare Access — needs an API token with Access: Apps and Policies = Edit
|
|
296
|
+
CLOUDFLARE_API_TOKEN=… node scripts/setup-access.mjs --dry-run
|
|
297
|
+
CLOUDFLARE_API_TOKEN=… node scripts/setup-access.mjs
|
|
298
|
+
|
|
299
|
+
CF_ACCESS_CLIENT_ID=… CF_ACCESS_CLIENT_SECRET=… \
|
|
300
|
+
./scripts/smoke.sh https://artifacts.dclregenesislabs.xyz "$DCL_ARTIFACTS_TOKEN"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`setup-access.mjs` is idempotent — it rewrites the applications and their
|
|
304
|
+
policies to match the table above, so re-run it after any policy change.
|
|
305
|
+
|
|
306
|
+
### Releasing the CLI to npm
|
|
307
|
+
|
|
308
|
+
The package is `cli/`, `shared/` and `templates/` — the `files` list in
|
|
309
|
+
`package.json`; nothing of the Worker ships. Releases go through
|
|
310
|
+
`.github/workflows/publish.yml` using npm **trusted publishing**: npm trusts
|
|
311
|
+
that workflow's GitHub OIDC identity, so there is no npm token in the repo, in
|
|
312
|
+
Actions secrets, or anywhere else, and every release carries provenance.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# bump "version" in package.json, commit, then:
|
|
316
|
+
git tag v1.2.3 && git push origin v1.2.3
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The trust is configured on npmjs.com under the package's *Settings → Trusted
|
|
320
|
+
Publisher* (organisation `dcl-regenesislabs`, repository
|
|
321
|
+
`ia-generated-artifacts`, workflow `publish.yml`). It keys on the workflow's
|
|
322
|
+
filename, so renaming the file breaks publishing until the setting is updated.
|
|
323
|
+
`npm pack --dry-run` shows exactly what a release would contain.
|
|
324
|
+
|
|
325
|
+
### Retiring the shared token
|
|
326
|
+
|
|
327
|
+
`UPLOAD_TOKEN` still works alongside per-user tokens so nobody's publishing
|
|
328
|
+
breaks mid-migration. Every API request logs its credential `kind`, so:
|
|
329
|
+
|
|
330
|
+
1. Watch for `"kind":"shared"` in `npm run tail`. When two weeks pass with none
|
|
331
|
+
— long enough to cover people who publish weekly — everybody has moved.
|
|
332
|
+
2. `wrangler secret put UPLOAD_TOKEN` with a value nobody holds. That is a
|
|
333
|
+
reversible kill switch; leave it a week.
|
|
334
|
+
3. Delete the shared branch in `src/auth.ts` and the `author` fallback in
|
|
335
|
+
`src/api.ts`.
|
|
336
|
+
|
|
337
|
+
Rotate it in any case. The old value is in this repo's git history, so removing
|
|
338
|
+
it from `HEAD` did not remove it.
|
|
339
|
+
|
|
340
|
+
## Caveats
|
|
341
|
+
|
|
342
|
+
- **Public URLs are genuinely public.** Anyone with a `/p/{alias}` link can open
|
|
343
|
+
it, with no login, until someone switches it off in the bar. Treat the switch
|
|
344
|
+
as publishing — the bar asks for confirmation and records who did it.
|
|
345
|
+
- **All artifacts share one origin.** A published page can read another artifact's
|
|
346
|
+
`localStorage`, and — since it runs as the signed-in viewer — call another
|
|
347
|
+
artifact's in-page API. Only people who can sign in with a work email can
|
|
348
|
+
publish pages, so this is the same trust as the gate itself; still, do not
|
|
349
|
+
publish pages that run untrusted third-party script.
|
|
350
|
+
- Access does not encrypt anything at rest — it gates the front door. R2 objects
|
|
351
|
+
are still readable by anyone with account access.
|
|
352
|
+
- A revoked token can still work for up to a minute: KV reads are cached at the
|
|
353
|
+
edge with a 60-second floor.
|
|
354
|
+
- Windows has no file modes, so `%APPDATA%\dcl-artifacts\credentials.json` is
|
|
355
|
+
protected by the default per-user ACL rather than by `600`.
|
|
356
|
+
- Limits: 25 MB per file, 2000 files per artifact, 1000 persisted checkboxes per
|
|
357
|
+
artifact.
|