@contentrail/cloudflare 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +103 -2
- package/dist/api/handler.d.ts +22 -0
- package/dist/api/handler.d.ts.map +1 -0
- package/dist/api/handler.js +408 -0
- package/dist/api/handler.js.map +1 -0
- package/dist/api/http.d.ts +16 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +69 -0
- package/dist/api/http.js.map +1 -0
- package/dist/auth/access.d.ts +21 -0
- package/dist/auth/access.d.ts.map +1 -0
- package/dist/auth/access.js +131 -0
- package/dist/auth/access.js.map +1 -0
- package/dist/auth/types.d.ts +23 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/authz/sites.d.ts +41 -0
- package/dist/authz/sites.d.ts.map +1 -0
- package/dist/authz/sites.js +60 -0
- package/dist/authz/sites.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ContentRail contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@contentrail/cloudflare`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server-only Cloudflare Worker publishing API with **Cloudflare Access** authentication and per-site authorization.
|
|
4
|
+
|
|
5
|
+
**Do not import from browser code.** Git/R2 credentials and Access verification settings stay in Worker secret bindings.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js **≥ 22** (Workers / Node-compatible server runtime)
|
|
10
|
+
- Depends on `@contentrail/core`, `@contentrail/publisher`, and `jose`
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
### Monorepo
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pnpm install
|
|
18
|
+
pnpm --filter @contentrail/cloudflare build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### From npm (upcoming `0.2.0` after authorized publish)
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pnpm add @contentrail/core@0.2.0 @contentrail/publisher@0.2.0 @contentrail/cloudflare@0.2.0 zod@^4.6.7
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## ADR 011b summary
|
|
28
|
+
|
|
29
|
+
- Authentication is pluggable; Access JWT is the first adapter.
|
|
30
|
+
- Authorization is separate and default-deny per `siteId`.
|
|
31
|
+
- Client requests never choose repository/bucket/prefix — only a configured `destinationRef` on the server.
|
|
32
|
+
|
|
33
|
+
## Endpoints
|
|
34
|
+
|
|
35
|
+
| Method | Path | Auth | Purpose |
|
|
36
|
+
| ------ | ---------------------------------------------- | ------------ | ------------------------------------------------------ |
|
|
37
|
+
| `GET` | `/health` | No | Liveness; no secrets |
|
|
38
|
+
| `GET` | `/v1/sites` | Yes | Sites the identity may access (no destination secrets) |
|
|
39
|
+
| `GET` | `/v1/sites/:siteId/config` | Yes | Editor-safe site metadata |
|
|
40
|
+
| `GET` | `/v1/sites/:siteId/collections/:name/content` | Yes (`read`) | Active published snapshot (`?entryId=` optional) |
|
|
41
|
+
| `POST` | `/v1/sites/:siteId/collections/:name/validate` | Yes | Validate entries |
|
|
42
|
+
| `POST` | `/v1/sites/:siteId/collections/:name/publish` | Yes | Publish via `@contentrail/publisher` |
|
|
43
|
+
| `GET` | `/v1/sites/:siteId/collections/:name/status` | Yes | Active version + retained versions |
|
|
44
|
+
| `POST` | `/v1/sites/:siteId/collections/:name/rollback` | Yes | Activate a prior version |
|
|
45
|
+
|
|
46
|
+
Publication success and rebuild status remain distinct fields in publish responses. Content reads use server `destinationRef` only and return the **active** published snapshot for that site/collection (ADR 018). Responses never include `destinationRef`, credentials, or inactive version bodies.
|
|
47
|
+
|
|
48
|
+
**Snapshot size:** `GET .../content` returns the full active entry set (optional `?entryId=`). This is acceptable for small/medium collections. Very large catalogues may hit Worker response or memory limits — pagination is deferred; use `entryId` or keep collections bounded for now.
|
|
49
|
+
|
|
50
|
+
## Configure Cloudflare Access
|
|
51
|
+
|
|
52
|
+
1. Create an Access application for the Worker hostname.
|
|
53
|
+
2. Note the application **AUD** tag and team domain (`<team>.cloudflareaccess.com`).
|
|
54
|
+
3. Bind Worker secrets (names only — never commit values):
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
CONTENTRAIL_ACCESS_TEAM_DOMAIN
|
|
58
|
+
CONTENTRAIL_ACCESS_AUD
|
|
59
|
+
# Plus existing publisher secrets, e.g. R2_* or GITHUB_TOKEN
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
4. Map identities to sites in Worker configuration (subjects/emails → permissions).
|
|
63
|
+
|
|
64
|
+
## Example wiring
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import {
|
|
68
|
+
createCloudflareAccessAuthenticator,
|
|
69
|
+
createPublishFetchHandler,
|
|
70
|
+
createSiteAuthorizer,
|
|
71
|
+
} from "@contentrail/cloudflare";
|
|
72
|
+
|
|
73
|
+
export default {
|
|
74
|
+
fetch: createPublishFetchHandler({
|
|
75
|
+
authenticator: createCloudflareAccessAuthenticator({
|
|
76
|
+
teamDomain: env.CONTENTRAIL_ACCESS_TEAM_DOMAIN,
|
|
77
|
+
audience: env.CONTENTRAIL_ACCESS_AUD,
|
|
78
|
+
}),
|
|
79
|
+
authorizer: createSiteAuthorizer({
|
|
80
|
+
sites: {
|
|
81
|
+
demo: {
|
|
82
|
+
destinationRef: "r2:sites/demo",
|
|
83
|
+
permissions: ["read", "publish", "status", "rollback", "validate"],
|
|
84
|
+
allowedEmails: ["owner@example.com"],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
collections: { articles },
|
|
89
|
+
corsAllowedOrigins: ["https://editor.example"],
|
|
90
|
+
getPublisher: (siteId, destinationRef) =>
|
|
91
|
+
createPublisherForBinding(env, destinationRef),
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Deferred
|
|
97
|
+
|
|
98
|
+
- File uploads (not required for Phase 4 JSON publishing)
|
|
99
|
+
- Live production deploy verification
|
|
100
|
+
- Editor UI (Phase 5)
|
|
101
|
+
|
|
102
|
+
## Credential rotation
|
|
103
|
+
|
|
104
|
+
Rotate Access application secrets / allowed identity lists in the Cloudflare dashboard, then update Worker bindings. Revoke by removing the identity from Access policies and from ContentRail site grants.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ContentCollection } from "@contentrail/core";
|
|
2
|
+
import { type ContentPublisher } from "@contentrail/publisher";
|
|
3
|
+
import type { Authenticator } from "../auth/types.js";
|
|
4
|
+
import type { SiteAuthorizer } from "../authz/sites.js";
|
|
5
|
+
export interface PublishApiOptions {
|
|
6
|
+
readonly authenticator: Authenticator;
|
|
7
|
+
readonly authorizer: SiteAuthorizer;
|
|
8
|
+
/** Resolve a publisher for a site using server-side bindings only. */
|
|
9
|
+
readonly getPublisher: (siteId: string, destinationRef: string) => ContentPublisher;
|
|
10
|
+
/** Collections available to the API; keyed by collection name. */
|
|
11
|
+
readonly collections: Readonly<Record<string, ContentCollection>>;
|
|
12
|
+
readonly corsAllowedOrigins?: readonly string[];
|
|
13
|
+
readonly maxBodyBytes?: number;
|
|
14
|
+
/** Optional safe logger — must not log tokens or full content payloads. */
|
|
15
|
+
readonly log?: (event: Record<string, unknown>) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates a Cloudflare Worker-compatible `fetch` handler for ContentRail publishing.
|
|
19
|
+
* Server-only. Does not expose provider credentials.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createPublishFetchHandler(options: PublishApiOptions): (request: Request) => Promise<Response>;
|
|
22
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAiB,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAGxE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,sEAAsE;IACtE,QAAQ,CAAC,YAAY,EAAE,CACrB,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,KACnB,gBAAgB,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAClE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACzD;AAMD;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,iBAAiB,GACzB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiRzC"}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { validateEntry } from "@contentrail/core";
|
|
2
|
+
import { PublishConflictError, PublishError, } from "@contentrail/publisher";
|
|
3
|
+
import { applyCors, getRequestId, jsonResponse, readJsonBody } from "./http.js";
|
|
4
|
+
const DEFAULT_MAX_BODY = 256_000;
|
|
5
|
+
/** Caps publish/validate entry lists independently of nested JSON field bounds. */
|
|
6
|
+
const DEFAULT_MAX_PUBLISH_ENTRIES = 500;
|
|
7
|
+
/**
|
|
8
|
+
* Creates a Cloudflare Worker-compatible `fetch` handler for ContentRail publishing.
|
|
9
|
+
* Server-only. Does not expose provider credentials.
|
|
10
|
+
*/
|
|
11
|
+
export function createPublishFetchHandler(options) {
|
|
12
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY;
|
|
13
|
+
const corsAllowedOrigins = options.corsAllowedOrigins ?? [];
|
|
14
|
+
return async (request) => {
|
|
15
|
+
const requestId = getRequestId(request);
|
|
16
|
+
const respond = (status, body) => applyCors(jsonResponse(status, body, { requestId }), request, corsAllowedOrigins);
|
|
17
|
+
if (request.method === "OPTIONS") {
|
|
18
|
+
return applyCors(new Response(null, {
|
|
19
|
+
status: 204,
|
|
20
|
+
headers: { "x-request-id": requestId },
|
|
21
|
+
}), request, corsAllowedOrigins);
|
|
22
|
+
}
|
|
23
|
+
const url = new URL(request.url);
|
|
24
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
25
|
+
return respond(200, { ok: true, service: "contentrail-publish-api" });
|
|
26
|
+
}
|
|
27
|
+
const auth = await options.authenticator.authenticate(request);
|
|
28
|
+
if (!auth.ok) {
|
|
29
|
+
options.log?.({
|
|
30
|
+
level: "warn",
|
|
31
|
+
requestId,
|
|
32
|
+
event: "auth_failed",
|
|
33
|
+
code: auth.code,
|
|
34
|
+
});
|
|
35
|
+
return respond(401, {
|
|
36
|
+
error: { code: auth.code, message: auth.message },
|
|
37
|
+
requestId,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (request.method === "GET" && url.pathname === "/v1/sites") {
|
|
41
|
+
return respond(200, {
|
|
42
|
+
sites: options.authorizer.listAccessibleSites(auth.identity),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const siteMatch = /^\/v1\/sites\/([^/]+)(?:\/(.*))?$/.exec(url.pathname);
|
|
46
|
+
if (siteMatch === null) {
|
|
47
|
+
return respond(404, {
|
|
48
|
+
error: { code: "NOT_FOUND", message: "Unknown route." },
|
|
49
|
+
requestId,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const siteId = decodeURIComponent(siteMatch[1]);
|
|
53
|
+
const rest = siteMatch[2] ?? "";
|
|
54
|
+
if (request.method === "GET" && rest === "config") {
|
|
55
|
+
const authz = options.authorizer.authorize(auth.identity, siteId, "status");
|
|
56
|
+
if (!authz.ok) {
|
|
57
|
+
return respond(authz.status, {
|
|
58
|
+
error: { code: authz.code, message: authz.message },
|
|
59
|
+
requestId,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return respond(200, {
|
|
63
|
+
siteId,
|
|
64
|
+
permissions: authz.binding.permissions,
|
|
65
|
+
collections: Object.keys(options.collections),
|
|
66
|
+
// Never return destinationRef or credentials.
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const collectionMatch = /^collections\/([^/]+)\/(validate|publish|status|rollback|content)$/.exec(rest);
|
|
70
|
+
if (collectionMatch === null) {
|
|
71
|
+
return respond(404, {
|
|
72
|
+
error: { code: "NOT_FOUND", message: "Unknown site route." },
|
|
73
|
+
requestId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const collectionName = decodeURIComponent(collectionMatch[1]);
|
|
77
|
+
const action = collectionMatch[2];
|
|
78
|
+
const permission = permissionFor(action);
|
|
79
|
+
const authz = options.authorizer.authorize(auth.identity, siteId, permission);
|
|
80
|
+
if (!authz.ok) {
|
|
81
|
+
return respond(authz.status, {
|
|
82
|
+
error: { code: authz.code, message: authz.message },
|
|
83
|
+
requestId,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const collection = options.collections[collectionName];
|
|
87
|
+
if (collection === undefined || collection.name !== collectionName) {
|
|
88
|
+
return respond(404, {
|
|
89
|
+
error: { code: "COLLECTION_NOT_FOUND", message: "Unknown collection." },
|
|
90
|
+
requestId,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const publisher = options.getPublisher(siteId, authz.binding.destinationRef);
|
|
94
|
+
try {
|
|
95
|
+
if (action === "content") {
|
|
96
|
+
if (request.method !== "GET") {
|
|
97
|
+
return respond(405, {
|
|
98
|
+
error: { code: "METHOD_NOT_ALLOWED", message: "Use GET." },
|
|
99
|
+
requestId,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const snapshot = await publisher.readActiveContent(siteId, collectionName);
|
|
103
|
+
if (snapshot === null) {
|
|
104
|
+
return respond(200, {
|
|
105
|
+
siteId,
|
|
106
|
+
collection: collectionName,
|
|
107
|
+
version: null,
|
|
108
|
+
publicationId: null,
|
|
109
|
+
createdAt: null,
|
|
110
|
+
entries: [],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const entryIdFilter = url.searchParams.get("entryId");
|
|
114
|
+
const entries = entryIdFilter === null
|
|
115
|
+
? snapshot.entries
|
|
116
|
+
: snapshot.entries.filter((entry) => entry.id === entryIdFilter);
|
|
117
|
+
if (entryIdFilter !== null && entries.length === 0) {
|
|
118
|
+
return respond(404, {
|
|
119
|
+
error: { code: "ENTRY_NOT_FOUND", message: "Entry not found." },
|
|
120
|
+
requestId,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return respond(200, {
|
|
124
|
+
siteId,
|
|
125
|
+
collection: collectionName,
|
|
126
|
+
version: snapshot.version,
|
|
127
|
+
publicationId: snapshot.publicationId,
|
|
128
|
+
createdAt: snapshot.createdAt,
|
|
129
|
+
entries,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (action === "status") {
|
|
133
|
+
if (request.method !== "GET") {
|
|
134
|
+
return respond(405, {
|
|
135
|
+
error: { code: "METHOD_NOT_ALLOWED", message: "Use GET." },
|
|
136
|
+
requestId,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const status = await publisher.getStatus(siteId, collectionName);
|
|
140
|
+
return respond(200, { siteId, collection: collectionName, ...status });
|
|
141
|
+
}
|
|
142
|
+
if (request.method !== "POST") {
|
|
143
|
+
return respond(405, {
|
|
144
|
+
error: { code: "METHOD_NOT_ALLOWED", message: "Use POST." },
|
|
145
|
+
requestId,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const body = await readJsonBody(request, maxBodyBytes);
|
|
149
|
+
if (!body.ok) {
|
|
150
|
+
return respond(body.status, {
|
|
151
|
+
error: { code: body.code, message: body.message },
|
|
152
|
+
requestId,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (action === "validate" || action === "publish") {
|
|
156
|
+
const parsed = parsePublishBody(body.value);
|
|
157
|
+
if (!parsed.ok) {
|
|
158
|
+
return respond(400, {
|
|
159
|
+
error: { code: parsed.code, message: parsed.message },
|
|
160
|
+
requestId,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (action === "validate") {
|
|
164
|
+
for (const entry of parsed.entries) {
|
|
165
|
+
const result = validateEntry(collection, entry.data);
|
|
166
|
+
if (!result.success) {
|
|
167
|
+
return respond(400, {
|
|
168
|
+
error: {
|
|
169
|
+
code: "VALIDATION",
|
|
170
|
+
message: result.error.message,
|
|
171
|
+
issues: result.error.issues,
|
|
172
|
+
entryId: entry.id,
|
|
173
|
+
},
|
|
174
|
+
requestId,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return respond(200, { ok: true, siteId, collection: collectionName });
|
|
179
|
+
}
|
|
180
|
+
const result = await publisher.publish({
|
|
181
|
+
siteId,
|
|
182
|
+
collection,
|
|
183
|
+
expectedVersion: parsed.expectedVersion,
|
|
184
|
+
entries: parsed.entries,
|
|
185
|
+
...(parsed.idempotencyKey !== undefined
|
|
186
|
+
? { idempotencyKey: parsed.idempotencyKey }
|
|
187
|
+
: {}),
|
|
188
|
+
...(parsed.publicationId !== undefined
|
|
189
|
+
? { publicationId: parsed.publicationId }
|
|
190
|
+
: {}),
|
|
191
|
+
...(parsed.message !== undefined ? { message: parsed.message } : {}),
|
|
192
|
+
});
|
|
193
|
+
options.log?.({
|
|
194
|
+
level: "info",
|
|
195
|
+
requestId,
|
|
196
|
+
event: "publish_success",
|
|
197
|
+
siteId,
|
|
198
|
+
collection: collectionName,
|
|
199
|
+
version: result.version,
|
|
200
|
+
rebuild: result.rebuild.state,
|
|
201
|
+
});
|
|
202
|
+
return respond(200, publicPublishResult(result));
|
|
203
|
+
}
|
|
204
|
+
if (action === "rollback") {
|
|
205
|
+
const parsed = parseRollbackBody(body.value);
|
|
206
|
+
if (!parsed.ok) {
|
|
207
|
+
return respond(400, {
|
|
208
|
+
error: { code: parsed.code, message: parsed.message },
|
|
209
|
+
requestId,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const result = await publisher.rollback({
|
|
213
|
+
siteId,
|
|
214
|
+
collectionName,
|
|
215
|
+
targetVersion: parsed.targetVersion,
|
|
216
|
+
expectedVersion: parsed.expectedVersion,
|
|
217
|
+
});
|
|
218
|
+
return respond(200, publicPublishResult(result));
|
|
219
|
+
}
|
|
220
|
+
return respond(404, {
|
|
221
|
+
error: { code: "NOT_FOUND", message: "Unknown action." },
|
|
222
|
+
requestId,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return mapPublishError(error, respond, requestId, options.log);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function permissionFor(action) {
|
|
231
|
+
if (action === "validate") {
|
|
232
|
+
return "validate";
|
|
233
|
+
}
|
|
234
|
+
if (action === "publish") {
|
|
235
|
+
return "publish";
|
|
236
|
+
}
|
|
237
|
+
if (action === "rollback") {
|
|
238
|
+
return "rollback";
|
|
239
|
+
}
|
|
240
|
+
if (action === "content") {
|
|
241
|
+
return "read";
|
|
242
|
+
}
|
|
243
|
+
return "status";
|
|
244
|
+
}
|
|
245
|
+
function publicPublishResult(result) {
|
|
246
|
+
return {
|
|
247
|
+
success: true,
|
|
248
|
+
publicationId: result.publicationId,
|
|
249
|
+
version: result.version,
|
|
250
|
+
previousVersion: result.previousVersion,
|
|
251
|
+
provider: result.provider,
|
|
252
|
+
rebuild: result.rebuild,
|
|
253
|
+
idempotentReplay: result.idempotentReplay,
|
|
254
|
+
manifest: {
|
|
255
|
+
collection: result.manifest.collection,
|
|
256
|
+
version: result.manifest.version,
|
|
257
|
+
entryIds: result.manifest.entryIds,
|
|
258
|
+
createdAt: result.manifest.createdAt,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function parsePublishBody(value) {
|
|
263
|
+
if (value === null || typeof value !== "object") {
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
code: "INVALID_BODY",
|
|
267
|
+
message: "JSON object required.",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const record = value;
|
|
271
|
+
if (!("expectedVersion" in record)) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
code: "INVALID_BODY",
|
|
275
|
+
message: 'Field "expectedVersion" is required (string or null).',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (record.expectedVersion !== null &&
|
|
279
|
+
typeof record.expectedVersion !== "string") {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
code: "INVALID_BODY",
|
|
283
|
+
message: 'Field "expectedVersion" must be a string or null.',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (!Array.isArray(record.entries)) {
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
code: "INVALID_BODY",
|
|
290
|
+
message: 'Field "entries" must be an array.',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (record.entries.length > DEFAULT_MAX_PUBLISH_ENTRIES) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
297
|
+
message: `Field "entries" exceeds ${DEFAULT_MAX_PUBLISH_ENTRIES} items.`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const entries = [];
|
|
301
|
+
for (const entry of record.entries) {
|
|
302
|
+
if (entry === null || typeof entry !== "object") {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
code: "INVALID_BODY",
|
|
306
|
+
message: "Each entry must be an object.",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const item = entry;
|
|
310
|
+
if (typeof item.id !== "string" || !("data" in item)) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
code: "INVALID_BODY",
|
|
314
|
+
message: 'Each entry requires string "id" and "data".',
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
entries.push({ id: item.id, data: item.data });
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
expectedVersion: record.expectedVersion,
|
|
322
|
+
entries,
|
|
323
|
+
...(typeof record.idempotencyKey === "string"
|
|
324
|
+
? { idempotencyKey: record.idempotencyKey }
|
|
325
|
+
: {}),
|
|
326
|
+
...(typeof record.publicationId === "string"
|
|
327
|
+
? { publicationId: record.publicationId }
|
|
328
|
+
: {}),
|
|
329
|
+
...(typeof record.message === "string" ? { message: record.message } : {}),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function parseRollbackBody(value) {
|
|
333
|
+
if (value === null || typeof value !== "object") {
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
code: "INVALID_BODY",
|
|
337
|
+
message: "JSON object required.",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const record = value;
|
|
341
|
+
if (typeof record.targetVersion !== "string") {
|
|
342
|
+
return {
|
|
343
|
+
ok: false,
|
|
344
|
+
code: "INVALID_BODY",
|
|
345
|
+
message: 'Field "targetVersion" is required.',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (!("expectedVersion" in record)) {
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
code: "INVALID_BODY",
|
|
352
|
+
message: 'Field "expectedVersion" is required.',
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (record.expectedVersion !== null &&
|
|
356
|
+
typeof record.expectedVersion !== "string") {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
code: "INVALID_BODY",
|
|
360
|
+
message: 'Field "expectedVersion" must be a string or null.',
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
ok: true,
|
|
365
|
+
targetVersion: record.targetVersion,
|
|
366
|
+
expectedVersion: record.expectedVersion,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function mapPublishError(error, respond, requestId, log) {
|
|
370
|
+
if (error instanceof PublishConflictError) {
|
|
371
|
+
return respond(409, {
|
|
372
|
+
error: {
|
|
373
|
+
code: "CONFLICT",
|
|
374
|
+
message: error.message,
|
|
375
|
+
currentVersion: error.currentVersion ?? null,
|
|
376
|
+
expectedVersion: error.expectedVersion ?? null,
|
|
377
|
+
},
|
|
378
|
+
requestId,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (error instanceof PublishError) {
|
|
382
|
+
const status = error.code === "VALIDATION"
|
|
383
|
+
? 400
|
|
384
|
+
: error.code === "PATH_ESCAPE"
|
|
385
|
+
? 400
|
|
386
|
+
: error.code === "NOT_FOUND"
|
|
387
|
+
? 404
|
|
388
|
+
: error.code === "CREDENTIALS"
|
|
389
|
+
? 500
|
|
390
|
+
: 502;
|
|
391
|
+
log?.({
|
|
392
|
+
level: "error",
|
|
393
|
+
requestId,
|
|
394
|
+
event: "publish_error",
|
|
395
|
+
code: error.code,
|
|
396
|
+
});
|
|
397
|
+
return respond(status, {
|
|
398
|
+
error: { code: error.code, message: error.message },
|
|
399
|
+
requestId,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
log?.({ level: "error", requestId, event: "unhandled_error" });
|
|
403
|
+
return respond(500, {
|
|
404
|
+
error: { code: "INTERNAL", message: "Unexpected server error." },
|
|
405
|
+
requestId,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function jsonResponse(status: number, body: unknown, init?: {
|
|
2
|
+
requestId: string;
|
|
3
|
+
headers?: HeadersInit;
|
|
4
|
+
}): Response;
|
|
5
|
+
export declare function getRequestId(request: Request): string;
|
|
6
|
+
export declare function readJsonBody(request: Request, maxBytes: number): Promise<{
|
|
7
|
+
ok: true;
|
|
8
|
+
value: unknown;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
status: number;
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function applyCors(response: Response, request: Request, allowedOrigins: readonly string[]): Response;
|
|
16
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,IAAI,GAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,WAAW,CAAA;CAE/C,GACA,QAAQ,CAKV;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAErD;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CACN;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAC/D,CA6CA;AAED,wBAAgB,SAAS,CACvB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,SAAS,MAAM,EAAE,GAChC,QAAQ,CAcV"}
|
package/dist/api/http.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export function jsonResponse(status, body, init = {
|
|
3
|
+
requestId: randomUUID(),
|
|
4
|
+
}) {
|
|
5
|
+
const headers = new Headers(init.headers);
|
|
6
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
7
|
+
headers.set("x-request-id", init.requestId);
|
|
8
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
9
|
+
}
|
|
10
|
+
export function getRequestId(request) {
|
|
11
|
+
return request.headers.get("x-request-id")?.trim() || randomUUID();
|
|
12
|
+
}
|
|
13
|
+
export async function readJsonBody(request, maxBytes) {
|
|
14
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
15
|
+
if (!contentType.toLowerCase().includes("application/json")) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
status: 415,
|
|
19
|
+
code: "UNSUPPORTED_MEDIA_TYPE",
|
|
20
|
+
message: "Content-Type must be application/json.",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const declaredLength = request.headers.get("content-length");
|
|
24
|
+
if (declaredLength !== null) {
|
|
25
|
+
const declared = Number(declaredLength);
|
|
26
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
status: 413,
|
|
30
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
31
|
+
message: `Payload exceeds ${maxBytes} bytes.`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const buffer = await request.arrayBuffer();
|
|
36
|
+
if (buffer.byteLength > maxBytes) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
status: 413,
|
|
40
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
41
|
+
message: `Payload exceeds ${maxBytes} bytes.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const text = new TextDecoder().decode(buffer);
|
|
46
|
+
return { ok: true, value: text === "" ? {} : JSON.parse(text) };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
status: 400,
|
|
52
|
+
code: "INVALID_JSON",
|
|
53
|
+
message: "Request body is not valid JSON.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function applyCors(response, request, allowedOrigins) {
|
|
58
|
+
const origin = request.headers.get("Origin");
|
|
59
|
+
if (origin === null || !allowedOrigins.includes(origin)) {
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
const headers = new Headers(response.headers);
|
|
63
|
+
headers.set("access-control-allow-origin", origin);
|
|
64
|
+
headers.set("vary", "Origin");
|
|
65
|
+
headers.set("access-control-allow-headers", "authorization, content-type, cf-access-jwt-assertion, x-request-id");
|
|
66
|
+
headers.set("access-control-allow-methods", "GET, POST, OPTIONS");
|
|
67
|
+
return new Response(response.body, { status: response.status, headers });
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as jose from "jose";
|
|
2
|
+
import type { Authenticator } from "./types.js";
|
|
3
|
+
export interface CloudflareAccessOptions {
|
|
4
|
+
/** Access team domain host, e.g. `myteam.cloudflareaccess.com`. */
|
|
5
|
+
readonly teamDomain: string;
|
|
6
|
+
/** Expected JWT audience (AUD tag from the Access application). */
|
|
7
|
+
readonly audience: string;
|
|
8
|
+
/** Optional allow-list of subjects and/or emails. Empty means any valid Access identity. */
|
|
9
|
+
readonly allowedSubjects?: readonly string[];
|
|
10
|
+
readonly allowedEmails?: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Injected JWKS loader for tests. Production uses the Access certs URL.
|
|
13
|
+
* Never log tokens.
|
|
14
|
+
*/
|
|
15
|
+
readonly getKey?: jose.JWTVerifyGetKey;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Cloudflare Access JWT authenticator. Fails closed when verification cannot complete.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createCloudflareAccessAuthenticator(options: CloudflareAccessOptions): Authenticator;
|
|
21
|
+
//# sourceMappingURL=access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/auth/access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAGV,aAAa,EAEd,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC;CACxC;AAoBD;;GAEG;AACH,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,uBAAuB,GAC/B,aAAa,CAqDf"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as jose from "jose";
|
|
2
|
+
function extractBearerOrAccessJwt(request) {
|
|
3
|
+
const accessHeader = request.headers.get("Cf-Access-Jwt-Assertion");
|
|
4
|
+
if (accessHeader !== null && accessHeader.trim() !== "") {
|
|
5
|
+
return accessHeader.trim();
|
|
6
|
+
}
|
|
7
|
+
const authorization = request.headers.get("Authorization");
|
|
8
|
+
if (authorization === null) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const match = /^Bearer\s+(\S+)/i.exec(authorization);
|
|
12
|
+
return match?.[1] ?? null;
|
|
13
|
+
}
|
|
14
|
+
function issuerFor(teamDomain) {
|
|
15
|
+
const host = teamDomain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
16
|
+
return `https://${host}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Cloudflare Access JWT authenticator. Fails closed when verification cannot complete.
|
|
20
|
+
*/
|
|
21
|
+
export function createCloudflareAccessAuthenticator(options) {
|
|
22
|
+
const issuer = issuerFor(options.teamDomain);
|
|
23
|
+
const jwksUrl = new URL(`${issuer}/cdn-cgi/access/certs`);
|
|
24
|
+
const getKey = options.getKey ?? jose.createRemoteJWKSet(jwksUrl);
|
|
25
|
+
return {
|
|
26
|
+
async authenticate(request) {
|
|
27
|
+
const token = extractBearerOrAccessJwt(request);
|
|
28
|
+
if (token === null) {
|
|
29
|
+
return failure("MISSING_CREDENTIAL", "Missing Access JWT credential.");
|
|
30
|
+
}
|
|
31
|
+
let payload;
|
|
32
|
+
try {
|
|
33
|
+
const verified = await jose.jwtVerify(token, getKey, {
|
|
34
|
+
issuer,
|
|
35
|
+
audience: options.audience,
|
|
36
|
+
clockTolerance: 5,
|
|
37
|
+
});
|
|
38
|
+
payload = verified.payload;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return mapJoseError(error);
|
|
42
|
+
}
|
|
43
|
+
const subject = payload.sub;
|
|
44
|
+
if (typeof subject !== "string" || subject.trim() === "") {
|
|
45
|
+
return failure("MALFORMED_CREDENTIAL", "Access token is missing a subject claim.");
|
|
46
|
+
}
|
|
47
|
+
const emailClaim = payload.email ?? payload.preferred_username;
|
|
48
|
+
const email = typeof emailClaim === "string" ? emailClaim.toLowerCase() : undefined;
|
|
49
|
+
const identity = {
|
|
50
|
+
subject,
|
|
51
|
+
issuer,
|
|
52
|
+
audience: options.audience,
|
|
53
|
+
...(email !== undefined ? { email } : {}),
|
|
54
|
+
};
|
|
55
|
+
if (!identityAllowed(identity, options)) {
|
|
56
|
+
return failure("IDENTITY_NOT_ALLOWED", "Authenticated identity is not permitted by installation policy.");
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, identity };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function identityAllowed(identity, options) {
|
|
63
|
+
const subjects = options.allowedSubjects ?? [];
|
|
64
|
+
const emails = options.allowedEmails ?? [];
|
|
65
|
+
if (subjects.length === 0 && emails.length === 0) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (subjects.includes(identity.subject)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (identity.email !== undefined &&
|
|
72
|
+
emails.map((value) => value.toLowerCase()).includes(identity.email)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
function mapJoseError(error) {
|
|
78
|
+
const code = typeof error === "object" &&
|
|
79
|
+
error !== null &&
|
|
80
|
+
"code" in error &&
|
|
81
|
+
typeof error.code === "string"
|
|
82
|
+
? error.code
|
|
83
|
+
: "";
|
|
84
|
+
const message = error instanceof Error ? error.message : "Token verification failed.";
|
|
85
|
+
const claim = typeof error === "object" &&
|
|
86
|
+
error !== null &&
|
|
87
|
+
"claim" in error &&
|
|
88
|
+
typeof error.claim === "string"
|
|
89
|
+
? error.claim
|
|
90
|
+
: "";
|
|
91
|
+
if (code === "ERR_JWKS_TIMEOUT" ||
|
|
92
|
+
code === "ERR_JWKS_INVALID" ||
|
|
93
|
+
code === "ERR_JOSE_GENERIC" ||
|
|
94
|
+
/JWKS|fetch|network|ENOTFOUND|ECONN|keyset/i.test(message)) {
|
|
95
|
+
return failure("KEY_UNAVAILABLE", "Unable to retrieve Access verification keys.");
|
|
96
|
+
}
|
|
97
|
+
if (code === "ERR_JWT_EXPIRED" || claim === "exp") {
|
|
98
|
+
return failure("EXPIRED", "Access token has expired.");
|
|
99
|
+
}
|
|
100
|
+
if (code === "ERR_JWT_CLAIM_VALIDATION_FAILED" &&
|
|
101
|
+
(claim === "aud" || /audience|"aud"/i.test(message))) {
|
|
102
|
+
return failure("INVALID_AUDIENCE", "Access token audience is invalid.");
|
|
103
|
+
}
|
|
104
|
+
if (code === "ERR_JWT_CLAIM_VALIDATION_FAILED" &&
|
|
105
|
+
(claim === "iss" || /issuer|"iss"/i.test(message))) {
|
|
106
|
+
return failure("INVALID_ISSUER", "Access token issuer is invalid.");
|
|
107
|
+
}
|
|
108
|
+
if (code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") {
|
|
109
|
+
return failure("INVALID_SIGNATURE", "Access token signature is invalid.");
|
|
110
|
+
}
|
|
111
|
+
if (code === "ERR_JWS_INVALID" ||
|
|
112
|
+
code === "ERR_JWT_INVALID" ||
|
|
113
|
+
code === "ERR_JWKS_NO_MATCHING_KEY") {
|
|
114
|
+
return failure("MALFORMED_CREDENTIAL", "Access token could not be verified.");
|
|
115
|
+
}
|
|
116
|
+
// Claim validation without a mapped claim, or unknown JOSE failures.
|
|
117
|
+
if (/audience|"aud"/i.test(message)) {
|
|
118
|
+
return failure("INVALID_AUDIENCE", "Access token audience is invalid.");
|
|
119
|
+
}
|
|
120
|
+
if (/issuer|"iss"/i.test(message)) {
|
|
121
|
+
return failure("INVALID_ISSUER", "Access token issuer is invalid.");
|
|
122
|
+
}
|
|
123
|
+
if (/signature/i.test(message)) {
|
|
124
|
+
return failure("INVALID_SIGNATURE", "Access token signature is invalid.");
|
|
125
|
+
}
|
|
126
|
+
return failure("MALFORMED_CREDENTIAL", "Access token could not be verified.");
|
|
127
|
+
}
|
|
128
|
+
function failure(code, message) {
|
|
129
|
+
return { ok: false, status: 401, code, message };
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/auth/access.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Identity {
|
|
2
|
+
/** Stable subject from the identity provider (e.g. Access `sub`). */
|
|
3
|
+
readonly subject: string;
|
|
4
|
+
readonly email?: string;
|
|
5
|
+
readonly issuer: string;
|
|
6
|
+
readonly audience?: string;
|
|
7
|
+
}
|
|
8
|
+
export type AuthenticationFailureCode = "MISSING_CREDENTIAL" | "MALFORMED_CREDENTIAL" | "INVALID_SIGNATURE" | "INVALID_ISSUER" | "INVALID_AUDIENCE" | "EXPIRED" | "KEY_UNAVAILABLE" | "IDENTITY_NOT_ALLOWED";
|
|
9
|
+
export interface AuthenticationSuccess {
|
|
10
|
+
readonly ok: true;
|
|
11
|
+
readonly identity: Identity;
|
|
12
|
+
}
|
|
13
|
+
export interface AuthenticationFailure {
|
|
14
|
+
readonly ok: false;
|
|
15
|
+
readonly status: 401;
|
|
16
|
+
readonly code: AuthenticationFailureCode;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
}
|
|
19
|
+
export type AuthenticationResult = AuthenticationSuccess | AuthenticationFailure;
|
|
20
|
+
export interface Authenticator {
|
|
21
|
+
authenticate(request: Request): Promise<AuthenticationResult>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,yBAAyB,GACjC,oBAAoB,GACpB,sBAAsB,GACtB,mBAAmB,GACnB,gBAAgB,GAChB,kBAAkB,GAClB,SAAS,GACT,iBAAiB,GACjB,sBAAsB,CAAC;AAE3B,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,oBAAoB,GAC5B,qBAAqB,GACrB,qBAAqB,CAAC;AAE1B,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAC/D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Identity } from "../auth/types.js";
|
|
2
|
+
export type SitePermission = "read" | "publish" | "status" | "rollback" | "validate";
|
|
3
|
+
export interface SiteBinding {
|
|
4
|
+
/**
|
|
5
|
+
* Server-owned publishing destination. Never taken from the client body.
|
|
6
|
+
* Opaque to HTTP clients — used only when constructing publishers.
|
|
7
|
+
*/
|
|
8
|
+
readonly destinationRef: string;
|
|
9
|
+
readonly permissions: readonly SitePermission[];
|
|
10
|
+
readonly allowedSubjects?: readonly string[];
|
|
11
|
+
readonly allowedEmails?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export interface SiteAuthorizationConfig {
|
|
14
|
+
readonly sites: Readonly<Record<string, SiteBinding>>;
|
|
15
|
+
}
|
|
16
|
+
export interface AuthorizationSuccess {
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
readonly siteId: string;
|
|
19
|
+
readonly binding: SiteBinding;
|
|
20
|
+
}
|
|
21
|
+
export interface AuthorizationFailure {
|
|
22
|
+
readonly ok: false;
|
|
23
|
+
readonly status: 403 | 404;
|
|
24
|
+
readonly code: "FORBIDDEN" | "SITE_NOT_FOUND";
|
|
25
|
+
readonly message: string;
|
|
26
|
+
}
|
|
27
|
+
export type AuthorizationResult = AuthorizationSuccess | AuthorizationFailure;
|
|
28
|
+
export interface AccessibleSiteSummary {
|
|
29
|
+
readonly siteId: string;
|
|
30
|
+
readonly permissions: readonly SitePermission[];
|
|
31
|
+
}
|
|
32
|
+
export interface SiteAuthorizer {
|
|
33
|
+
authorize(identity: Identity, siteId: string, permission: SitePermission): AuthorizationResult;
|
|
34
|
+
/** Sites this identity may access (any grant), without destination secrets. */
|
|
35
|
+
listAccessibleSites(identity: Identity): readonly AccessibleSiteSummary[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Default-deny site authorizer. Identity must match site grants and permissions.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createSiteAuthorizer(config: SiteAuthorizationConfig): SiteAuthorizer;
|
|
41
|
+
//# sourceMappingURL=sites.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sites.d.ts","sourceRoot":"","sources":["../../src/authz/sites.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,MAAM,cAAc,GACtB,MAAM,GACN,SAAS,GACT,QAAQ,GACR,UAAU,GACV,UAAU,CAAC;AAEf,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;IAChD,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,gBAAgB,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAE9E,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CACP,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,cAAc,GACzB,mBAAmB,CAAC;IACvB,+EAA+E;IAC/E,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,qBAAqB,EAAE,CAAC;CAC3E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,uBAAuB,GAC9B,cAAc,CA4ChB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default-deny site authorizer. Identity must match site grants and permissions.
|
|
3
|
+
*/
|
|
4
|
+
export function createSiteAuthorizer(config) {
|
|
5
|
+
return {
|
|
6
|
+
authorize(identity, siteId, permission) {
|
|
7
|
+
const binding = config.sites[siteId];
|
|
8
|
+
if (binding === undefined) {
|
|
9
|
+
return {
|
|
10
|
+
ok: false,
|
|
11
|
+
status: 404,
|
|
12
|
+
code: "SITE_NOT_FOUND",
|
|
13
|
+
message: "Site not found.",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (!identityMatches(identity, binding)) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
status: 403,
|
|
20
|
+
code: "FORBIDDEN",
|
|
21
|
+
message: "Not authorised for this site.",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (!binding.permissions.includes(permission)) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
status: 403,
|
|
28
|
+
code: "FORBIDDEN",
|
|
29
|
+
message: "Permission denied for this operation.",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { ok: true, siteId, binding };
|
|
33
|
+
},
|
|
34
|
+
listAccessibleSites(identity) {
|
|
35
|
+
const sites = [];
|
|
36
|
+
for (const [siteId, binding] of Object.entries(config.sites)) {
|
|
37
|
+
if (identityMatches(identity, binding)) {
|
|
38
|
+
sites.push({ siteId, permissions: binding.permissions });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return sites.sort((a, b) => a.siteId.localeCompare(b.siteId));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function identityMatches(identity, binding) {
|
|
46
|
+
const subjects = binding.allowedSubjects ?? [];
|
|
47
|
+
const emails = binding.allowedEmails ?? [];
|
|
48
|
+
if (subjects.length === 0 && emails.length === 0) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (subjects.includes(identity.subject)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (identity.email !== undefined &&
|
|
55
|
+
emails.map((value) => value.toLowerCase()).includes(identity.email)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=sites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sites.js","sourceRoot":"","sources":["../../src/authz/sites.ts"],"names":[],"mappings":"AAsDA;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA+B;IAE/B,OAAO;QACL,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU;YACpC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACrC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,MAAM,EAAE,GAAG;oBACX,IAAI,EAAE,gBAAgB;oBACtB,OAAO,EAAE,iBAAiB;iBAC3B,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC;gBACxC,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,MAAM,EAAE,GAAG;oBACX,IAAI,EAAE,WAAW;oBACjB,OAAO,EAAE,+BAA+B;iBACzC,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC9C,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,MAAM,EAAE,GAAG;oBACX,IAAI,EAAE,WAAW;oBACjB,OAAO,EAAE,uCAAuC;iBACjD,CAAC;YACJ,CAAC;YAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QACvC,CAAC;QAED,mBAAmB,CAAC,QAAQ;YAC1B,MAAM,KAAK,GAA4B,EAAE,CAAC;YAC1C,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC7D,IAAI,eAAe,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC;oBACvC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;gBAC3D,CAAC;YACH,CAAC;YACD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QAChE,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,QAAkB,EAAE,OAAoB;IAC/D,MAAM,QAAQ,GAAG,OAAO,CAAC,eAAe,IAAI,EAAE,CAAC;IAC/C,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;IAC3C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACxC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IACE,QAAQ,CAAC,KAAK,KAAK,SAAS;QAC5B,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,EACnE,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @contentrail/cloudflare — server-only Worker publishing API.
|
|
3
|
+
* Do not import from browser bundles. Secrets stay in Worker bindings.
|
|
4
|
+
*/
|
|
5
|
+
export { createCloudflareAccessAuthenticator, type CloudflareAccessOptions, } from "./auth/access.js";
|
|
6
|
+
export type { AuthenticationFailure, AuthenticationFailureCode, AuthenticationResult, AuthenticationSuccess, Authenticator, Identity, } from "./auth/types.js";
|
|
7
|
+
export { createSiteAuthorizer, type AccessibleSiteSummary, type AuthorizationFailure, type AuthorizationResult, type AuthorizationSuccess, type SiteAuthorizer, type SiteAuthorizationConfig, type SiteBinding, type SitePermission, } from "./authz/sites.js";
|
|
8
|
+
export { createPublishFetchHandler, type PublishApiOptions, } from "./api/handler.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,mCAAmC,EACnC,KAAK,uBAAuB,GAC7B,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,qBAAqB,EACrB,yBAAyB,EACzB,oBAAoB,EACpB,qBAAqB,EACrB,aAAa,EACb,QAAQ,GACT,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,cAAc,GACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,yBAAyB,EACzB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @contentrail/cloudflare — server-only Worker publishing API.
|
|
3
|
+
* Do not import from browser bundles. Secrets stay in Worker bindings.
|
|
4
|
+
*/
|
|
5
|
+
export { createCloudflareAccessAuthenticator, } from "./auth/access.js";
|
|
6
|
+
export { createSiteAuthorizer, } from "./authz/sites.js";
|
|
7
|
+
export { createPublishFetchHandler, } from "./api/handler.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,mCAAmC,GAEpC,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EACL,oBAAoB,GASrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,yBAAyB,GAE1B,MAAM,kBAAkB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentrail/cloudflare",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Server-only Cloudflare Worker publishing API with Access auth and site authorization.",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"node": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"jose": "^6.2.14",
|
|
27
|
+
"@contentrail/core": "0.2.0",
|
|
28
|
+
"@contentrail/publisher": "0.2.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.20.5",
|
|
32
|
+
"typescript": "5.9.3",
|
|
33
|
+
"zod": "4.6.7"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/Hridhin-k/contentrail.git",
|
|
38
|
+
"directory": "packages/cloudflare"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/Hridhin-k/contentrail/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/Hridhin-k/contentrail#readme",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.json",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
50
|
+
"test": "pnpm run build && tsc -p tsconfig.test.json && node --test test-dist/**/*.test.js"
|
|
51
|
+
}
|
|
6
52
|
}
|