@we8/cloudflare 0.1.2 → 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.
Files changed (50) hide show
  1. package/README.md +99 -6
  2. package/dist/access.d.ts +129 -0
  3. package/dist/access.d.ts.map +1 -0
  4. package/dist/access.js +211 -0
  5. package/dist/access.js.map +1 -0
  6. package/dist/admin-auth.d.ts +42 -0
  7. package/dist/admin-auth.d.ts.map +1 -0
  8. package/dist/admin-auth.js +19 -0
  9. package/dist/admin-auth.js.map +1 -0
  10. package/dist/cli-args.d.ts +8 -2
  11. package/dist/cli-args.d.ts.map +1 -1
  12. package/dist/cli-args.js +28 -4
  13. package/dist/cli-args.js.map +1 -1
  14. package/dist/cli.d.ts +1 -1
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +89 -6
  17. package/dist/cli.js.map +1 -1
  18. package/dist/doctor.d.ts +79 -3
  19. package/dist/doctor.d.ts.map +1 -1
  20. package/dist/doctor.js +250 -15
  21. package/dist/doctor.js.map +1 -1
  22. package/dist/index.d.ts +21 -9
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +20 -9
  25. package/dist/index.js.map +1 -1
  26. package/dist/jwt.d.ts +103 -0
  27. package/dist/jwt.d.ts.map +1 -0
  28. package/dist/jwt.js +265 -0
  29. package/dist/jwt.js.map +1 -0
  30. package/dist/perimeter.d.ts +49 -0
  31. package/dist/perimeter.d.ts.map +1 -0
  32. package/dist/perimeter.js +54 -0
  33. package/dist/perimeter.js.map +1 -0
  34. package/dist/project.d.ts +7 -0
  35. package/dist/project.d.ts.map +1 -1
  36. package/dist/project.js +31 -0
  37. package/dist/project.js.map +1 -1
  38. package/dist/skill-command.d.ts +48 -0
  39. package/dist/skill-command.d.ts.map +1 -0
  40. package/dist/skill-command.js +122 -0
  41. package/dist/skill-command.js.map +1 -0
  42. package/dist/skill.d.ts +51 -0
  43. package/dist/skill.d.ts.map +1 -0
  44. package/dist/skill.js +229 -0
  45. package/dist/skill.js.map +1 -0
  46. package/dist/wrangler-config.d.ts +64 -1
  47. package/dist/wrangler-config.d.ts.map +1 -1
  48. package/dist/wrangler-config.js +133 -21
  49. package/dist/wrangler-config.js.map +1 -1
  50. 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. Three things, and only three:
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
- - **The wrangler config generator.** `generateWranglerConfig(answers)` returns
13
- the `wrangler.jsonc` a project deploys with, shaped after the CMS package's
14
- own config. `create-we8` writes a new project's config with it.
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`; `seed` loads the development seed into the local database and
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
@@ -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"}
@@ -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; --remote\n 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 help | --version\n";
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
@@ -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;AAE7E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAC;IACrB,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAC;IAChB,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,KAAK,2sBAiBjB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAoDhF"}
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"}