@we8/cloudflare 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -6
- package/dist/access.d.ts +129 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +211 -0
- package/dist/access.js.map +1 -0
- package/dist/admin-auth.d.ts +42 -0
- package/dist/admin-auth.d.ts.map +1 -0
- package/dist/admin-auth.js +19 -0
- package/dist/admin-auth.js.map +1 -0
- package/dist/cli-args.d.ts +8 -2
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +28 -4
- package/dist/cli-args.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +89 -6
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +79 -3
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +250 -15
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +21 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -9
- package/dist/index.js.map +1 -1
- package/dist/jwt.d.ts +103 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +265 -0
- package/dist/jwt.js.map +1 -0
- package/dist/perimeter.d.ts +49 -0
- package/dist/perimeter.d.ts.map +1 -0
- package/dist/perimeter.js +54 -0
- package/dist/perimeter.js.map +1 -0
- package/dist/project.d.ts +7 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +31 -0
- package/dist/project.js.map +1 -1
- package/dist/skill-command.d.ts +48 -0
- package/dist/skill-command.d.ts.map +1 -0
- package/dist/skill-command.js +122 -0
- package/dist/skill-command.js.map +1 -0
- package/dist/skill.d.ts +51 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +229 -0
- package/dist/skill.js.map +1 -0
- package/dist/wrangler-config.d.ts +64 -1
- package/dist/wrangler-config.d.ts.map +1 -1
- package/dist/wrangler-config.js +133 -21
- package/dist/wrangler-config.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# @we8/cloudflare
|
|
2
2
|
|
|
3
|
-
The Cloudflare pack for a we8 CMS project.
|
|
3
|
+
The Cloudflare pack for a we8 CMS project. Four things, and only four:
|
|
4
4
|
|
|
5
|
+
- **Two admin auth providers**, for sites that do not want a user table.
|
|
6
|
+
`cloudflareAccessAdminAuth()` validates the assertion a Cloudflare Access
|
|
7
|
+
application forwards; `perimeterAdminAuth({ acknowledged: true })` checks
|
|
8
|
+
nothing at all, because something in front of the worker already did. See
|
|
9
|
+
[Admin auth](#admin-auth) below.
|
|
5
10
|
- **A real email sender.** `cloudflareEmailSender` plugs into the CMS email
|
|
6
11
|
seam and sends over Cloudflare Email Service. It is off until a site sets
|
|
7
12
|
`EMAIL_MODE=cloudflare`, binds `send_email` as `EMAIL`, and sets
|
|
@@ -9,13 +14,16 @@ The Cloudflare pack for a we8 CMS project. Three things, and only three:
|
|
|
9
14
|
behaviour, so installing the pack never changes how a site works. Sending
|
|
10
15
|
never throws: mail from a CMS is always a side effect of something that
|
|
11
16
|
already succeeded.
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
- **Two generators.** `generateWranglerConfig(answers)` returns the
|
|
18
|
+
`wrangler.jsonc` a project deploys with, shaped after the CMS package's own
|
|
19
|
+
config. `generateSkill({ mode, site })` returns the project skill an agentic
|
|
20
|
+
tool loads, written for the auth mode the project is actually in.
|
|
21
|
+
`create-we8` writes both into a new project.
|
|
15
22
|
- **The `we8-cloudflare` CLI.** `doctor` says what is missing and the exact
|
|
16
23
|
command that fixes it; `migrate` applies the migrations that ship inside
|
|
17
|
-
`@we8/cms
|
|
18
|
-
refuses, always, to touch a deployed one
|
|
24
|
+
`@we8/cms` and then every installed provider's; `seed` loads the development
|
|
25
|
+
seed into the local database and refuses, always, to touch a deployed one;
|
|
26
|
+
`skill` writes or refreshes the project skill in place.
|
|
19
27
|
|
|
20
28
|
```
|
|
21
29
|
npx @we8/cloudflare doctor # what is missing, and how to fix it
|
|
@@ -23,8 +31,29 @@ npx @we8/cloudflare doctor --remote # the same, including deployed secrets
|
|
|
23
31
|
npx @we8/cloudflare migrate # apply migrations locally
|
|
24
32
|
npx @we8/cloudflare migrate --remote # apply them to the deployed database
|
|
25
33
|
npx @we8/cloudflare seed # local development data, local only
|
|
34
|
+
npx @we8/cloudflare skill # write .claude/skills/we8/SKILL.md
|
|
35
|
+
npx @we8/cloudflare skill --check # exit 1 if it is missing or stale
|
|
26
36
|
```
|
|
27
37
|
|
|
38
|
+
## The project skill
|
|
39
|
+
|
|
40
|
+
`skill` writes `.claude/skills/we8/SKILL.md`, the operating manual an agentic
|
|
41
|
+
coding tool loads when it opens the project: the composition, the commands,
|
|
42
|
+
the auth model with its own mode's guardrails, the key model, the API, AEO,
|
|
43
|
+
and the standing guardrails. `create-we8` writes it at scaffold time; this
|
|
44
|
+
command is how a project that predates it adopts one, and how a project whose
|
|
45
|
+
auth registration changed refreshes it.
|
|
46
|
+
|
|
47
|
+
Nothing is asked for. The mode is read out of the worker entry with its
|
|
48
|
+
comments stripped, the site workspace is a directory that either exists or
|
|
49
|
+
does not, and a perimeter registration that has not acknowledged itself is
|
|
50
|
+
reported as such. A directory that is not a we8 project is refused rather than
|
|
51
|
+
written a skill that would be a guess.
|
|
52
|
+
|
|
53
|
+
`--check` writes nothing and exits 1 when the file is missing or out of date,
|
|
54
|
+
which is what belongs in CI. `doctor` warns, and never fails, when the file is
|
|
55
|
+
absent.
|
|
56
|
+
|
|
28
57
|
## Requirements
|
|
29
58
|
|
|
30
59
|
- Peer dependencies, declared and auto-installed by npm: `@we8/cms` (the
|
|
@@ -33,6 +62,70 @@ npx @we8/cloudflare seed # local development data, local only
|
|
|
33
62
|
- Node 20 or newer.
|
|
34
63
|
- A Cloudflare account only when you deploy; local development runs entirely
|
|
35
64
|
offline.
|
|
65
|
+
- For `cloudflareAccessAdminAuth`: a Cloudflare Zero Trust account with an
|
|
66
|
+
Access application in front of this Worker's hostname. Nothing else; the
|
|
67
|
+
provider has no dependencies of its own and verifies on WebCrypto.
|
|
68
|
+
|
|
69
|
+
## Admin auth
|
|
70
|
+
|
|
71
|
+
`@we8/cms` has no admin auth of its own. A worker that registers no provider
|
|
72
|
+
refuses every `/v1/admin/*` request, which is the secure default; `@we8/auth`
|
|
73
|
+
is the embedded provider most self-hosters want, and these two are for the
|
|
74
|
+
sites that do not want a user table at all.
|
|
75
|
+
|
|
76
|
+
### Cloudflare Access
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import cms, { registerAdminAuth } from '@we8/cms';
|
|
80
|
+
import { cloudflareAccessAdminAuth } from '@we8/cloudflare';
|
|
81
|
+
|
|
82
|
+
registerAdminAuth(cloudflareAccessAdminAuth());
|
|
83
|
+
|
|
84
|
+
export default cms;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Put an Access application in front of the Worker's hostname, and Cloudflare
|
|
88
|
+
does the signing in: the policy decides who reaches the worker, and every
|
|
89
|
+
request it forwards carries a signed assertion naming the person. This
|
|
90
|
+
provider verifies that assertion against the team's published keys, for this
|
|
91
|
+
application's audience, from this team's issuer, and inside its own expiry,
|
|
92
|
+
before it becomes an identity. Being behind Access is not what it trusts, so a
|
|
93
|
+
request that arrives by another route carrying a copied, forged, or expired
|
|
94
|
+
token is refused exactly like one carrying none.
|
|
95
|
+
|
|
96
|
+
Four vars configure it, none of them secret:
|
|
97
|
+
|
|
98
|
+
| Var | Required | What it is |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `ACCESS_TEAM_DOMAIN` | yes | `acme`, `acme.cloudflareaccess.com`, or the full URL |
|
|
101
|
+
| `ACCESS_AUD` | yes | The Access application's Application Audience tag |
|
|
102
|
+
| `ACCESS_OWNER_EMAILS` | one of the two | The addresses that hold the owner role, comma-separated |
|
|
103
|
+
| `ACCESS_OWNER_DEFAULT` | one of the two | `true` makes every identity Access admits an owner |
|
|
104
|
+
|
|
105
|
+
Any of them may be passed to `cloudflareAccessAdminAuth({ ... })` instead;
|
|
106
|
+
what is passed wins, and what is left out is read from the environment.
|
|
107
|
+
Everyone Access admits who is not named an owner is a member, which is the
|
|
108
|
+
CMS's editorial role. `GET /v1/auth/mode` reports `external` with sign-up
|
|
109
|
+
closed: the site's accounts are not this worker's to create.
|
|
110
|
+
|
|
111
|
+
### Perimeter mode
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
registerAdminAuth(perimeterAdminAuth({ acknowledged: true }));
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Every request that reaches `/v1/admin/*` is the owner. There is nothing else
|
|
118
|
+
to it, and that is both the point and the risk: if the perimeter ever opens (a
|
|
119
|
+
policy edited, a hostname exposed, the `workers.dev` route left enabled), the
|
|
120
|
+
whole admin API opens with it, the form inbox and its personal data included.
|
|
121
|
+
It is the right answer behind a VPN, a company gateway, or an Access
|
|
122
|
+
application with no role mapping to give this worker, and the wrong answer
|
|
123
|
+
everywhere else.
|
|
124
|
+
|
|
125
|
+
`acknowledged: true` is how a project says it read that paragraph.
|
|
126
|
+
`we8-cloudflare doctor` warns about perimeter mode always and
|
|
127
|
+
`doctor --remote` fails a deployment that has not acknowledged it.
|
|
128
|
+
|
|
36
129
|
## The guardrail
|
|
37
130
|
|
|
38
131
|
`seed/local.sql` contains plaintext API keys, because that is what a
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cloudflare Access admin auth provider.
|
|
3
|
+
*
|
|
4
|
+
* Put the admin behind a Cloudflare Access application and Cloudflare does the
|
|
5
|
+
* signing in: the policy decides who reaches the worker at all, and every
|
|
6
|
+
* request it lets through carries a signed assertion naming the person. This
|
|
7
|
+
* provider validates that assertion and turns it into the identity the CMS
|
|
8
|
+
* gates on, so a site gets a real admin login with no user table, no password
|
|
9
|
+
* to reset, and no session for this worker to sign.
|
|
10
|
+
*
|
|
11
|
+
* import cms, { registerAdminAuth } from '@we8/cms';
|
|
12
|
+
* import { cloudflareAccessAdminAuth } from '@we8/cloudflare';
|
|
13
|
+
*
|
|
14
|
+
* registerAdminAuth(cloudflareAccessAdminAuth());
|
|
15
|
+
* export default cms;
|
|
16
|
+
*
|
|
17
|
+
* Being behind Access is NOT what this trusts. The assertion is verified
|
|
18
|
+
* against the team's published keys, for this application's audience, and
|
|
19
|
+
* within its own expiry, so a request that reaches the worker by another route
|
|
20
|
+
* carrying a copied, forged, or expired token is refused exactly like one
|
|
21
|
+
* carrying none. See jwt.ts for the verification itself.
|
|
22
|
+
*
|
|
23
|
+
* It claims no routes and offers no first-account flow: `GET /v1/auth/mode`
|
|
24
|
+
* reports `external` with sign-up closed, because the site's accounts are not
|
|
25
|
+
* this worker's to create.
|
|
26
|
+
*/
|
|
27
|
+
import type { AdminAuthProvider, UserRole } from './admin-auth.js';
|
|
28
|
+
/** What `GET /v1/auth/mode` reports this provider as. */
|
|
29
|
+
export declare const ACCESS_PROVIDER_NAME = "cloudflare-access";
|
|
30
|
+
/** The header Cloudflare Access adds to every request it forwards. */
|
|
31
|
+
export declare const ACCESS_JWT_HEADER = "Cf-Access-Jwt-Assertion";
|
|
32
|
+
/** The cookie Access also sets, read only when the header is absent. */
|
|
33
|
+
export declare const ACCESS_JWT_COOKIE = "CF_Authorization";
|
|
34
|
+
/**
|
|
35
|
+
* What this provider needs to know. Every field may instead be a var on the
|
|
36
|
+
* worker, which is where the scaffolder puts them: a config value given here
|
|
37
|
+
* wins, and anything left out is read from the environment at request time.
|
|
38
|
+
*
|
|
39
|
+
* The environment is the default because none of these are secrets (a team
|
|
40
|
+
* domain is public and an AUD tag identifies an application rather than
|
|
41
|
+
* authorising anything), because `wrangler.jsonc` is where a project's
|
|
42
|
+
* configuration already lives, and because the doctor can then check them.
|
|
43
|
+
*/
|
|
44
|
+
export interface CloudflareAccessConfig {
|
|
45
|
+
/**
|
|
46
|
+
* The Access team domain. `acme`, `acme.cloudflareaccess.com`, and
|
|
47
|
+
* `https://acme.cloudflareaccess.com` are all accepted and mean the same
|
|
48
|
+
* thing. It gives both the JWKS URL and the issuer.
|
|
49
|
+
*/
|
|
50
|
+
teamDomain?: string;
|
|
51
|
+
/** The Access application's AUD tag, from its configuration page. */
|
|
52
|
+
audience?: string;
|
|
53
|
+
/** The emails that hold the owner role. Everyone else Access admits is a member. */
|
|
54
|
+
owners?: readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* Every identity Access admits is an owner. For the solo operator whose
|
|
57
|
+
* Access policy already names exactly one person, where an owners list would
|
|
58
|
+
* be the same name written twice.
|
|
59
|
+
*/
|
|
60
|
+
ownerByDefault?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** The vars this provider reads when the config leaves a value out. */
|
|
63
|
+
export interface CloudflareAccessEnv {
|
|
64
|
+
ACCESS_TEAM_DOMAIN?: string | undefined;
|
|
65
|
+
ACCESS_AUD?: string | undefined;
|
|
66
|
+
/** Comma-separated. Whitespace around an address is ignored. */
|
|
67
|
+
ACCESS_OWNER_EMAILS?: string | undefined;
|
|
68
|
+
/** `true` makes every admitted identity an owner. */
|
|
69
|
+
ACCESS_OWNER_DEFAULT?: string | undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `acme`, `acme.cloudflareaccess.com`, and a full URL all become
|
|
73
|
+
* `acme.cloudflareaccess.com`, so the issuer and the JWKS URL are built from
|
|
74
|
+
* one normalized value however the operator wrote it down.
|
|
75
|
+
*/
|
|
76
|
+
export declare function normalizeTeamDomain(input: string): string;
|
|
77
|
+
/** Where the team publishes the public half of its signing keys. */
|
|
78
|
+
export declare function accessCertsUrl(teamDomain: string): string;
|
|
79
|
+
/** The `iss` every assertion from this team carries. */
|
|
80
|
+
export declare function accessIssuer(teamDomain: string): string;
|
|
81
|
+
/** The resolved settings one request is validated against. */
|
|
82
|
+
export interface AccessSettings {
|
|
83
|
+
teamDomain: string;
|
|
84
|
+
certsUrl: string;
|
|
85
|
+
issuer: string;
|
|
86
|
+
audience: string;
|
|
87
|
+
owners: string[];
|
|
88
|
+
ownerByDefault: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Merges the config and the environment into the settings a request is checked
|
|
92
|
+
* against, or says what is missing. A placeholder left in `wrangler.jsonc`
|
|
93
|
+
* counts as missing: it is what the scaffolder writes when nobody has filled
|
|
94
|
+
* the value in yet, and treating it as a real team domain would only turn a
|
|
95
|
+
* clear "not configured" into a confusing "does not verify".
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveAccessSettings(config: CloudflareAccessConfig, env: unknown): {
|
|
98
|
+
ok: true;
|
|
99
|
+
settings: AccessSettings;
|
|
100
|
+
} | {
|
|
101
|
+
ok: false;
|
|
102
|
+
reason: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The assertion on a request: the header Access adds, or the cookie it sets
|
|
106
|
+
* when something reached the worker without it. Null when neither is present,
|
|
107
|
+
* which the CMS turns into a plain 401.
|
|
108
|
+
*/
|
|
109
|
+
export declare function readAssertion(request: Request): string | null;
|
|
110
|
+
/**
|
|
111
|
+
* Owner when the address is on the list (or the site says every identity
|
|
112
|
+
* owns it), member otherwise. Least privilege: an address nobody named gets
|
|
113
|
+
* the editorial role, never the administrative one.
|
|
114
|
+
*/
|
|
115
|
+
export declare function accessRole(email: string, settings: AccessSettings): UserRole;
|
|
116
|
+
/** Forgets what has already been logged. Tests use it; the request path does not. */
|
|
117
|
+
export declare function clearAccessReports(): void;
|
|
118
|
+
/**
|
|
119
|
+
* The provider, ready to register.
|
|
120
|
+
*
|
|
121
|
+
* Everything it cannot do, it refuses. An absent assertion, a token that does
|
|
122
|
+
* not verify, an identity with no email claim (an Access service token, for
|
|
123
|
+
* instance, which is a machine and not an administrator), and a configuration
|
|
124
|
+
* this worker cannot make sense of all resolve to null, which the CMS answers
|
|
125
|
+
* as a 401. The public API is untouched either way: only the admin surface
|
|
126
|
+
* depends on any of this.
|
|
127
|
+
*/
|
|
128
|
+
export declare function cloudflareAccessAdminAuth(config?: CloudflareAccessConfig): AdminAuthProvider;
|
|
129
|
+
//# sourceMappingURL=access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAiB,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAUlF,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AAExD,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAE3D,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,qBAAqB,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,qDAAqD;IACrD,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMzD;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wDAAwD;AACxD,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,OAAO,CAAC;CACzB;AAgBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,sBAAsB,EAC9B,GAAG,EAAE,OAAO,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA6CxE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAc7D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,QAAQ,CAG5E;AAeD,qFAAqF;AACrF,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,GAAE,sBAA2B,GAClC,iBAAiB,CAuCnB"}
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cloudflare Access admin auth provider.
|
|
3
|
+
*
|
|
4
|
+
* Put the admin behind a Cloudflare Access application and Cloudflare does the
|
|
5
|
+
* signing in: the policy decides who reaches the worker at all, and every
|
|
6
|
+
* request it lets through carries a signed assertion naming the person. This
|
|
7
|
+
* provider validates that assertion and turns it into the identity the CMS
|
|
8
|
+
* gates on, so a site gets a real admin login with no user table, no password
|
|
9
|
+
* to reset, and no session for this worker to sign.
|
|
10
|
+
*
|
|
11
|
+
* import cms, { registerAdminAuth } from '@we8/cms';
|
|
12
|
+
* import { cloudflareAccessAdminAuth } from '@we8/cloudflare';
|
|
13
|
+
*
|
|
14
|
+
* registerAdminAuth(cloudflareAccessAdminAuth());
|
|
15
|
+
* export default cms;
|
|
16
|
+
*
|
|
17
|
+
* Being behind Access is NOT what this trusts. The assertion is verified
|
|
18
|
+
* against the team's published keys, for this application's audience, and
|
|
19
|
+
* within its own expiry, so a request that reaches the worker by another route
|
|
20
|
+
* carrying a copied, forged, or expired token is refused exactly like one
|
|
21
|
+
* carrying none. See jwt.ts for the verification itself.
|
|
22
|
+
*
|
|
23
|
+
* It claims no routes and offers no first-account flow: `GET /v1/auth/mode`
|
|
24
|
+
* reports `external` with sign-up closed, because the site's accounts are not
|
|
25
|
+
* this worker's to create.
|
|
26
|
+
*/
|
|
27
|
+
import { verifyAccessJwt } from './jwt.js';
|
|
28
|
+
import { ACCESS_AUD_VAR, ACCESS_OWNER_DEFAULT_VAR, ACCESS_OWNER_EMAILS_VAR, ACCESS_TEAM_DOMAIN_VAR, isPlaceholder, } from './wrangler-config.js';
|
|
29
|
+
/** What `GET /v1/auth/mode` reports this provider as. */
|
|
30
|
+
export const ACCESS_PROVIDER_NAME = 'cloudflare-access';
|
|
31
|
+
/** The header Cloudflare Access adds to every request it forwards. */
|
|
32
|
+
export const ACCESS_JWT_HEADER = 'Cf-Access-Jwt-Assertion';
|
|
33
|
+
/** The cookie Access also sets, read only when the header is absent. */
|
|
34
|
+
export const ACCESS_JWT_COOKIE = 'CF_Authorization';
|
|
35
|
+
/**
|
|
36
|
+
* `acme`, `acme.cloudflareaccess.com`, and a full URL all become
|
|
37
|
+
* `acme.cloudflareaccess.com`, so the issuer and the JWKS URL are built from
|
|
38
|
+
* one normalized value however the operator wrote it down.
|
|
39
|
+
*/
|
|
40
|
+
export function normalizeTeamDomain(input) {
|
|
41
|
+
let value = input.trim().toLowerCase();
|
|
42
|
+
value = value.replace(/^https?:\/\//, '');
|
|
43
|
+
value = value.replace(/\/.*$/, '');
|
|
44
|
+
if (value.length === 0)
|
|
45
|
+
return '';
|
|
46
|
+
return value.endsWith('.cloudflareaccess.com') ? value : `${value}.cloudflareaccess.com`;
|
|
47
|
+
}
|
|
48
|
+
/** Where the team publishes the public half of its signing keys. */
|
|
49
|
+
export function accessCertsUrl(teamDomain) {
|
|
50
|
+
return `https://${normalizeTeamDomain(teamDomain)}/cdn-cgi/access/certs`;
|
|
51
|
+
}
|
|
52
|
+
/** The `iss` every assertion from this team carries. */
|
|
53
|
+
export function accessIssuer(teamDomain) {
|
|
54
|
+
return `https://${normalizeTeamDomain(teamDomain)}`;
|
|
55
|
+
}
|
|
56
|
+
function parseOwners(value) {
|
|
57
|
+
if (!value)
|
|
58
|
+
return [];
|
|
59
|
+
return value
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
62
|
+
.filter((entry) => entry.length > 0 && !isPlaceholder(entry));
|
|
63
|
+
}
|
|
64
|
+
function configured(value) {
|
|
65
|
+
if (typeof value !== 'string')
|
|
66
|
+
return undefined;
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
return trimmed.length === 0 || isPlaceholder(trimmed) ? undefined : trimmed;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Merges the config and the environment into the settings a request is checked
|
|
72
|
+
* against, or says what is missing. A placeholder left in `wrangler.jsonc`
|
|
73
|
+
* counts as missing: it is what the scaffolder writes when nobody has filled
|
|
74
|
+
* the value in yet, and treating it as a real team domain would only turn a
|
|
75
|
+
* clear "not configured" into a confusing "does not verify".
|
|
76
|
+
*/
|
|
77
|
+
export function resolveAccessSettings(config, env) {
|
|
78
|
+
const bindings = (typeof env === 'object' && env !== null ? env : {});
|
|
79
|
+
const teamDomain = configured(config.teamDomain) ?? configured(bindings.ACCESS_TEAM_DOMAIN);
|
|
80
|
+
if (!teamDomain) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
reason: `no team domain: pass teamDomain to cloudflareAccessAdminAuth, or set vars.${ACCESS_TEAM_DOMAIN_VAR}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const audience = configured(config.audience) ?? configured(bindings.ACCESS_AUD);
|
|
87
|
+
if (!audience) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
reason: `no audience: pass audience to cloudflareAccessAdminAuth, or set vars.${ACCESS_AUD_VAR}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const owners = config.owners !== undefined
|
|
94
|
+
? config.owners.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0)
|
|
95
|
+
: parseOwners(bindings.ACCESS_OWNER_EMAILS);
|
|
96
|
+
const ownerByDefault = config.ownerByDefault ?? bindings.ACCESS_OWNER_DEFAULT?.trim().toLowerCase() === 'true';
|
|
97
|
+
if (owners.length === 0 && !ownerByDefault) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
reason: `nobody would be an owner: set vars.${ACCESS_OWNER_EMAILS_VAR} to the owner addresses, or vars.${ACCESS_OWNER_DEFAULT_VAR} to "true" if every identity Access admits should own this site`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
settings: {
|
|
106
|
+
teamDomain: normalizeTeamDomain(teamDomain),
|
|
107
|
+
certsUrl: accessCertsUrl(teamDomain),
|
|
108
|
+
issuer: accessIssuer(teamDomain),
|
|
109
|
+
audience,
|
|
110
|
+
owners,
|
|
111
|
+
ownerByDefault,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The assertion on a request: the header Access adds, or the cookie it sets
|
|
117
|
+
* when something reached the worker without it. Null when neither is present,
|
|
118
|
+
* which the CMS turns into a plain 401.
|
|
119
|
+
*/
|
|
120
|
+
export function readAssertion(request) {
|
|
121
|
+
const header = request.headers.get(ACCESS_JWT_HEADER);
|
|
122
|
+
if (header && header.trim().length > 0)
|
|
123
|
+
return header.trim();
|
|
124
|
+
const cookies = request.headers.get('Cookie');
|
|
125
|
+
if (!cookies)
|
|
126
|
+
return null;
|
|
127
|
+
for (const part of cookies.split(';')) {
|
|
128
|
+
const eq = part.indexOf('=');
|
|
129
|
+
if (eq <= 0)
|
|
130
|
+
continue;
|
|
131
|
+
if (part.slice(0, eq).trim() !== ACCESS_JWT_COOKIE)
|
|
132
|
+
continue;
|
|
133
|
+
const value = part.slice(eq + 1).trim();
|
|
134
|
+
if (value.length > 0)
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Owner when the address is on the list (or the site says every identity
|
|
141
|
+
* owns it), member otherwise. Least privilege: an address nobody named gets
|
|
142
|
+
* the editorial role, never the administrative one.
|
|
143
|
+
*/
|
|
144
|
+
export function accessRole(email, settings) {
|
|
145
|
+
if (settings.ownerByDefault)
|
|
146
|
+
return 'owner';
|
|
147
|
+
return settings.owners.includes(email.trim().toLowerCase()) ? 'owner' : 'member';
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Logged once per distinct reason per isolate. A misconfiguration is the same
|
|
151
|
+
* on every request, and repeating it once per request would bury the rest of
|
|
152
|
+
* the log without telling the operator anything new.
|
|
153
|
+
*/
|
|
154
|
+
const reported = new Set();
|
|
155
|
+
function reportOnce(message) {
|
|
156
|
+
if (reported.has(message))
|
|
157
|
+
return;
|
|
158
|
+
reported.add(message);
|
|
159
|
+
console.error(`[access] ${message}`);
|
|
160
|
+
}
|
|
161
|
+
/** Forgets what has already been logged. Tests use it; the request path does not. */
|
|
162
|
+
export function clearAccessReports() {
|
|
163
|
+
reported.clear();
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The provider, ready to register.
|
|
167
|
+
*
|
|
168
|
+
* Everything it cannot do, it refuses. An absent assertion, a token that does
|
|
169
|
+
* not verify, an identity with no email claim (an Access service token, for
|
|
170
|
+
* instance, which is a machine and not an administrator), and a configuration
|
|
171
|
+
* this worker cannot make sense of all resolve to null, which the CMS answers
|
|
172
|
+
* as a 401. The public API is untouched either way: only the admin surface
|
|
173
|
+
* depends on any of this.
|
|
174
|
+
*/
|
|
175
|
+
export function cloudflareAccessAdminAuth(config = {}) {
|
|
176
|
+
return {
|
|
177
|
+
name: ACCESS_PROVIDER_NAME,
|
|
178
|
+
async resolve(request, env) {
|
|
179
|
+
const resolved = resolveAccessSettings(config, env);
|
|
180
|
+
if (!resolved.ok) {
|
|
181
|
+
reportOnce(`${resolved.reason}; the admin API is refused until it is`);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const token = readAssertion(request);
|
|
185
|
+
if (!token)
|
|
186
|
+
return null;
|
|
187
|
+
const verification = await verifyAccessJwt({
|
|
188
|
+
token,
|
|
189
|
+
certsUrl: resolved.settings.certsUrl,
|
|
190
|
+
issuer: resolved.settings.issuer,
|
|
191
|
+
audience: resolved.settings.audience,
|
|
192
|
+
});
|
|
193
|
+
if (!verification.ok) {
|
|
194
|
+
console.warn(`[access] refused an assertion: ${verification.reason}`);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const email = verification.claims.email;
|
|
198
|
+
if (typeof email !== 'string' || email.length === 0) {
|
|
199
|
+
console.warn('[access] the assertion carries no email claim, so it names no administrator');
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const sub = verification.claims.sub;
|
|
203
|
+
return {
|
|
204
|
+
userId: typeof sub === 'string' && sub.length > 0 ? sub : email.toLowerCase(),
|
|
205
|
+
email,
|
|
206
|
+
role: accessRole(email, resolved.settings),
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The admin auth seam, restated.
|
|
3
|
+
*
|
|
4
|
+
* These four declarations mirror `AdminIdentity`, `AdminAuthProvider`,
|
|
5
|
+
* `FirstRunState`, and `UserRole` in `@we8/cms`. They are restated here for the
|
|
6
|
+
* same reason `EmailSender` is (see email.ts): the pack declares the CMS as a
|
|
7
|
+
* peer rather than a dependency, so the two packages typecheck and build in
|
|
8
|
+
* any order, and neither has to exist before the other. The interfaces are
|
|
9
|
+
* structural, so a provider shaped like this one satisfies the CMS's seam
|
|
10
|
+
* wherever a composed worker registers it, and the generated project's own
|
|
11
|
+
* `npm run typecheck` is what proves the two still line up.
|
|
12
|
+
*
|
|
13
|
+
* The division of labour the CMS documents holds here too: a provider says WHO
|
|
14
|
+
* a request is, and the core decides WHAT that identity may do. Nothing in this
|
|
15
|
+
* file, and nothing in either provider beside it, has an opinion about the
|
|
16
|
+
* owner and member semantics; those live in `@we8/cms` and stay there.
|
|
17
|
+
*/
|
|
18
|
+
/** The two roles the CMS knows. Mirrors `UserRole` in `@we8/cms`. */
|
|
19
|
+
export type UserRole = 'owner' | 'member';
|
|
20
|
+
/**
|
|
21
|
+
* Who a provider says this request is. `email` is optional because not every
|
|
22
|
+
* identity source has one; it is what publish history is attributed to.
|
|
23
|
+
*/
|
|
24
|
+
export interface AdminIdentity {
|
|
25
|
+
userId: string;
|
|
26
|
+
role: UserRole;
|
|
27
|
+
email?: string | null;
|
|
28
|
+
}
|
|
29
|
+
/** Whether a provider still offers a way to create the very first account. */
|
|
30
|
+
export type FirstRunState = 'open' | 'closed';
|
|
31
|
+
/** Mirrors `AdminAuthProvider` in `@we8/cms`. */
|
|
32
|
+
export interface AdminAuthProvider {
|
|
33
|
+
/** What `GET /v1/auth/mode` reports the provider as. */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Resolves the request to an identity, or null when it carries none. */
|
|
36
|
+
resolve(request: Request, env: unknown): Promise<AdminIdentity | null>;
|
|
37
|
+
/** Claims routes of the provider's own. Neither provider here claims any. */
|
|
38
|
+
handleRoute?(request: Request, env: unknown): Promise<Response | null>;
|
|
39
|
+
/** Whether a first-account flow is available. Neither provider here has one. */
|
|
40
|
+
firstRun?(env: unknown): Promise<FirstRunState>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=admin-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-auth.d.ts","sourceRoot":"","sources":["../src/admin-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,qEAAqE;AACrE,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE9C,iDAAiD;AACjD,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACvE,6EAA6E;IAC7E,WAAW,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACvE,gFAAgF;IAChF,QAAQ,CAAC,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACjD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The admin auth seam, restated.
|
|
3
|
+
*
|
|
4
|
+
* These four declarations mirror `AdminIdentity`, `AdminAuthProvider`,
|
|
5
|
+
* `FirstRunState`, and `UserRole` in `@we8/cms`. They are restated here for the
|
|
6
|
+
* same reason `EmailSender` is (see email.ts): the pack declares the CMS as a
|
|
7
|
+
* peer rather than a dependency, so the two packages typecheck and build in
|
|
8
|
+
* any order, and neither has to exist before the other. The interfaces are
|
|
9
|
+
* structural, so a provider shaped like this one satisfies the CMS's seam
|
|
10
|
+
* wherever a composed worker registers it, and the generated project's own
|
|
11
|
+
* `npm run typecheck` is what proves the two still line up.
|
|
12
|
+
*
|
|
13
|
+
* The division of labour the CMS documents holds here too: a provider says WHO
|
|
14
|
+
* a request is, and the core decides WHAT that identity may do. Nothing in this
|
|
15
|
+
* file, and nothing in either provider beside it, has an opinion about the
|
|
16
|
+
* owner and member semantics; those live in `@we8/cms` and stay there.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=admin-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-auth.js","sourceRoot":"","sources":["../src/admin-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG"}
|
package/dist/cli-args.d.ts
CHANGED
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
* Argument parsing for `we8-cloudflare`, kept apart from the CLI that spawns
|
|
3
3
|
* wrangler so the shapes below can be tested without a subprocess.
|
|
4
4
|
*/
|
|
5
|
-
export type CommandName = 'doctor' | 'migrate' | 'seed' | 'help' | 'version';
|
|
5
|
+
export type CommandName = 'doctor' | 'migrate' | 'seed' | 'skill' | 'help' | 'version';
|
|
6
6
|
export interface ParsedCommand {
|
|
7
7
|
command: CommandName;
|
|
8
8
|
/** Where the project lives. Defaults to the working directory. */
|
|
9
9
|
dir: string;
|
|
10
10
|
/** True when the command should act on the deployed database. */
|
|
11
11
|
remote: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* `skill --check`: report instead of writing, and exit non-zero when the
|
|
14
|
+
* file is missing or out of date. One flag rather than two spellings, so a
|
|
15
|
+
* CI step and a doctor's fix line name the same thing.
|
|
16
|
+
*/
|
|
17
|
+
check: boolean;
|
|
12
18
|
/** Set when the arguments cannot be honoured. The CLI prints it and exits 1. */
|
|
13
19
|
error?: string;
|
|
14
20
|
}
|
|
15
|
-
export declare const USAGE = "we8-cloudflare - the Cloudflare pack for a we8 CMS project\n\nUsage:\n we8-cloudflare doctor [--remote] [--dir <path>]\n Check the project's configuration and print what is missing, with the\n command that fixes each thing. --remote also checks the deployed\n Worker's secrets.\n\n we8-cloudflare migrate [--remote] [--dir <path>]\n Apply the @we8/cms migrations in order. Local by default
|
|
21
|
+
export declare const USAGE = "we8-cloudflare - the Cloudflare pack for a we8 CMS project\n\nUsage:\n we8-cloudflare doctor [--remote] [--dir <path>]\n Check the project's configuration and print what is missing, with the\n command that fixes each thing. --remote also checks the deployed\n Worker's secrets.\n\n we8-cloudflare migrate [--remote] [--dir <path>]\n Apply the @we8/cms migrations in order, then the migrations of every\n installed provider package (today: @we8/auth). Local by default;\n --remote applies them to the deployed D1 database.\n\n we8-cloudflare seed [--dir <path>]\n Load the development seed into the LOCAL database. It contains plaintext\n API keys, so this command has no remote mode and refuses to be given one.\n\n we8-cloudflare skill [--check] [--dir <path>]\n Write .claude/skills/we8/SKILL.md, the operating manual an agentic tool\n loads when it opens this project, generated from the project's own auth\n mode and workspace. --check writes nothing and exits 1 when the file is\n missing or out of date, which is what belongs in CI.\n\n we8-cloudflare help | --version\n";
|
|
16
22
|
/**
|
|
17
23
|
* Parses argv (without the node and script entries). Unknown flags are an
|
|
18
24
|
* error rather than a silent no-op, because a typo in `--remote` on a migrate
|
package/dist/cli-args.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAEvF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAC;IACrB,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,KAAK,0nCAwBjB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAsEhF"}
|