@olmocms/front 0.1.2 → 0.1.6
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 +30 -44
- package/dist/auth/action.cjs +106 -0
- package/dist/auth/action.cjs.map +1 -0
- package/dist/auth/action.d.cts +7 -0
- package/dist/auth/action.d.ts +7 -0
- package/dist/auth/action.js +82 -0
- package/dist/auth/action.js.map +1 -0
- package/dist/chunk-UCR7WW3X.js +169 -0
- package/dist/chunk-UCR7WW3X.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/components/index.cjs +93 -0
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.cts +18 -1
- package/dist/components/index.d.ts +18 -1
- package/dist/components/index.js +92 -0
- package/dist/components/index.js.map +1 -1
- package/dist/index.cjs +197 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +96 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.cjs +106 -2
- package/dist/middleware/index.cjs.map +1 -1
- package/dist/middleware/index.d.cts +3 -1
- package/dist/middleware/index.d.ts +3 -1
- package/dist/middleware/index.js +5 -3
- package/package.json +9 -3
- package/dist/chunk-GQITFXCV.js +0 -66
- package/dist/chunk-GQITFXCV.js.map +0 -1
package/README.md
CHANGED
|
@@ -9,8 +9,7 @@ Shared utilities, React components, SEO helpers, middleware, and CLI scaffold to
|
|
|
9
9
|
- [Package entries](#package-entries)
|
|
10
10
|
- [Installation](#installation)
|
|
11
11
|
- [Local development (watch mode)](#local-development-watch-mode)
|
|
12
|
-
- [
|
|
13
|
-
- [Versioning](#versioning)
|
|
12
|
+
- [Releasing a new version](#releasing-a-new-version)
|
|
14
13
|
- [CLI — olmo-front](#cli--olmo-front)
|
|
15
14
|
- [Entry point reference](#entry-point-reference)
|
|
16
15
|
|
|
@@ -91,71 +90,58 @@ Then run `npm install`.
|
|
|
91
90
|
|
|
92
91
|
---
|
|
93
92
|
|
|
94
|
-
##
|
|
93
|
+
## Releasing a new version
|
|
95
94
|
|
|
96
|
-
|
|
95
|
+
Releases are automated by GitLab CI: pushing a `vX.Y.Z` git tag triggers `npm publish` to the public npm registry. You do **not** run `npm publish` manually for normal releases.
|
|
97
96
|
|
|
98
|
-
-
|
|
99
|
-
- npm auth token (Publish scope) stored in `.npmrc`
|
|
97
|
+
### Day-to-day release flow
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
From a clean working tree on `main`:
|
|
102
100
|
|
|
103
101
|
```bash
|
|
104
|
-
|
|
102
|
+
npm version patch # or minor / major — bumps package.json AND creates a vX.Y.Z git tag
|
|
103
|
+
git push origin main --follow-tags
|
|
105
104
|
```
|
|
106
105
|
|
|
107
|
-
|
|
106
|
+
That's the whole release. GitLab CI then:
|
|
108
107
|
|
|
109
|
-
|
|
108
|
+
1. Runs `typecheck` and `build` on every push (see `.gitlab-ci.yml`).
|
|
109
|
+
2. On a tag matching `^v\d+\.\d+\.\d+$`, runs the `publish` job, which writes `NPM_TOKEN` into `~/.npmrc` and runs `npm publish`. `publishConfig.access: "public"` in `package.json` makes it a public scoped publish.
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
npm run build
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Produces:
|
|
116
|
-
- `dist/*.js` — ESM bundles
|
|
117
|
-
- `dist/*.cjs` — CommonJS bundles
|
|
118
|
-
- `dist/*.d.ts` — TypeScript declarations
|
|
119
|
-
- `dist/cli/index.js` — CLI binary
|
|
120
|
-
|
|
121
|
-
### Step 3 — Publish
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
npm publish
|
|
125
|
-
```
|
|
111
|
+
Watch the pipeline at **GitLab → CI/CD → Pipelines**. When green, verify on **https://www.npmjs.com/package/@olmocms/front**.
|
|
126
112
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
### Step 4 — Verify
|
|
130
|
-
|
|
131
|
-
Check **https://www.npmjs.com/package/@olmocms/front** for the new version.
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## Versioning
|
|
113
|
+
### Version bump rules
|
|
136
114
|
|
|
137
115
|
Follow semantic versioning (`MAJOR.MINOR.PATCH`):
|
|
138
116
|
|
|
139
|
-
| Change
|
|
117
|
+
| Change | Bump | Example |
|
|
140
118
|
|---|---|---|
|
|
141
119
|
| Breaking API change | MAJOR | `0.x.x` → `1.0.0` |
|
|
142
120
|
| New entry point or new export | MINOR | `0.1.0` → `0.2.0` |
|
|
143
121
|
| Bug fix, internal refactor | PATCH | `0.1.0` → `0.1.1` |
|
|
144
122
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
123
|
+
npm rejects re-publishing an existing version, so always bump first.
|
|
124
|
+
|
|
125
|
+
### One-time setup (reference — already done)
|
|
126
|
+
|
|
127
|
+
Kept here so the wiring is recoverable if the project is ever cloned fresh or the token rotates:
|
|
150
128
|
|
|
151
|
-
|
|
129
|
+
- **npm.js org** — the `@olmocms` organization owns the scope on npmjs.com.
|
|
130
|
+
- **npm automation token** — generated under *Account → Access Tokens → Generate New Token → Granular Access Token* (or a classic *Automation* token), scoped to publish `@olmocms/*`. Automation tokens bypass 2FA, which CI requires.
|
|
131
|
+
- **GitLab CI/CD variable** — token stored as `NPM_TOKEN` under *Settings → CI/CD → Variables*, marked **Masked** and **Protected**.
|
|
132
|
+
- **GitLab protected tag** — `v*` added under *Settings → Repository → Protected tags*, so only protected refs can read `NPM_TOKEN`.
|
|
133
|
+
- **`.gitlab-ci.yml`** — defines `typecheck`, `build`, and the tag-gated `publish` job.
|
|
134
|
+
|
|
135
|
+
### Manual publish (fallback only)
|
|
136
|
+
|
|
137
|
+
Only needed for the very first publish of a new scope or if CI is broken:
|
|
152
138
|
|
|
153
139
|
```bash
|
|
154
|
-
npm
|
|
140
|
+
npm login
|
|
141
|
+
npm run build
|
|
142
|
+
npm publish
|
|
155
143
|
```
|
|
156
144
|
|
|
157
|
-
npm does not allow overwriting an existing version — always increment before publishing.
|
|
158
|
-
|
|
159
145
|
---
|
|
160
146
|
|
|
161
147
|
## CLI — olmo-front
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/auth/action.ts
|
|
22
|
+
var action_exports = {};
|
|
23
|
+
__export(action_exports, {
|
|
24
|
+
stageLoginAction: () => stageLoginAction
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(action_exports);
|
|
27
|
+
var import_headers = require("next/headers");
|
|
28
|
+
|
|
29
|
+
// src/auth/stage.ts
|
|
30
|
+
var COOKIE_NAME = "olmo_stage_auth";
|
|
31
|
+
var DEFAULT_SESSION_DAYS = 7;
|
|
32
|
+
function readStageEnv() {
|
|
33
|
+
const username = process.env.OLMO_STAGE_USERNAME;
|
|
34
|
+
const password = process.env.OLMO_STAGE_PASSWORD;
|
|
35
|
+
const secret = process.env.OLMO_STAGE_SECRET;
|
|
36
|
+
if (!username || !password || !secret) return null;
|
|
37
|
+
const days = Number(process.env.OLMO_STAGE_SESSION_DAYS);
|
|
38
|
+
return {
|
|
39
|
+
username,
|
|
40
|
+
password,
|
|
41
|
+
secret,
|
|
42
|
+
sessionDays: Number.isFinite(days) && days > 0 ? days : DEFAULT_SESSION_DAYS
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function getStageCookieName() {
|
|
46
|
+
return COOKIE_NAME;
|
|
47
|
+
}
|
|
48
|
+
function toBase64Url(bytes) {
|
|
49
|
+
const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
50
|
+
let str = "";
|
|
51
|
+
for (let i = 0; i < arr.length; i++) str += String.fromCharCode(arr[i]);
|
|
52
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
53
|
+
}
|
|
54
|
+
async function importKey(secret) {
|
|
55
|
+
return crypto.subtle.importKey(
|
|
56
|
+
"raw",
|
|
57
|
+
new TextEncoder().encode(secret),
|
|
58
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
59
|
+
false,
|
|
60
|
+
["sign", "verify"]
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
async function signSession(payload, secret) {
|
|
64
|
+
const body = toBase64Url(new TextEncoder().encode(JSON.stringify(payload)));
|
|
65
|
+
const key = await importKey(secret);
|
|
66
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body));
|
|
67
|
+
return `${body}.${toBase64Url(sig)}`;
|
|
68
|
+
}
|
|
69
|
+
function verifyCredentials(env, username, password) {
|
|
70
|
+
if (typeof username !== "string" || typeof password !== "string") return false;
|
|
71
|
+
return username === env.username && password === env.password;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/auth/action.ts
|
|
75
|
+
function safeRedirectTarget(raw) {
|
|
76
|
+
if (typeof raw !== "string" || raw.length === 0) return "/";
|
|
77
|
+
if (!raw.startsWith("/") || raw.startsWith("//")) return "/";
|
|
78
|
+
return raw;
|
|
79
|
+
}
|
|
80
|
+
async function stageLoginAction(_prev, formData) {
|
|
81
|
+
const env = readStageEnv();
|
|
82
|
+
if (!env) return { error: "missing_config" };
|
|
83
|
+
const username = formData.get("username");
|
|
84
|
+
const password = formData.get("password");
|
|
85
|
+
if (!verifyCredentials(env, username, password)) {
|
|
86
|
+
return { error: "invalid_credentials" };
|
|
87
|
+
}
|
|
88
|
+
const exp = Date.now() + env.sessionDays * 24 * 60 * 60 * 1e3;
|
|
89
|
+
const token = await signSession({ u: env.username, exp }, env.secret);
|
|
90
|
+
const store = await (0, import_headers.cookies)();
|
|
91
|
+
store.set(getStageCookieName(), token, {
|
|
92
|
+
httpOnly: true,
|
|
93
|
+
// secure must be false on HTTP dev (next dev on localhost) so the browser stores
|
|
94
|
+
// the cookie. In production builds (next start / Vercel) this becomes true.
|
|
95
|
+
secure: process.env.NODE_ENV === "production",
|
|
96
|
+
sameSite: "lax",
|
|
97
|
+
path: "/",
|
|
98
|
+
expires: new Date(exp)
|
|
99
|
+
});
|
|
100
|
+
return { redirectTo: safeRedirectTarget(formData.get("from")) };
|
|
101
|
+
}
|
|
102
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
103
|
+
0 && (module.exports = {
|
|
104
|
+
stageLoginAction
|
|
105
|
+
});
|
|
106
|
+
//# sourceMappingURL=action.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/action.ts","../../src/auth/stage.ts"],"sourcesContent":["// 'use server' is injected by the tsup banner — keep this file directive-free\n// so the bundled output has exactly one directive at the top.\nimport { cookies } from 'next/headers';\nimport { getStageCookieName, readStageEnv, signSession, verifyCredentials } from './stage.js';\n\nexport type StageLoginState = {\n error?: 'missing_config' | 'invalid_credentials' | null;\n redirectTo?: string | null;\n};\n\nfunction safeRedirectTarget(raw: unknown): string {\n if (typeof raw !== 'string' || raw.length === 0) return '/';\n if (!raw.startsWith('/') || raw.startsWith('//')) return '/';\n return raw;\n}\n\nexport async function stageLoginAction(_prev: StageLoginState, formData: FormData): Promise<StageLoginState> {\n const env = readStageEnv();\n if (!env) return { error: 'missing_config' };\n\n const username = formData.get('username');\n const password = formData.get('password');\n if (!verifyCredentials(env, username, password)) {\n return { error: 'invalid_credentials' };\n }\n\n const exp = Date.now() + env.sessionDays * 24 * 60 * 60 * 1000;\n const token = await signSession({ u: env.username, exp }, env.secret);\n\n const store = await cookies();\n store.set(getStageCookieName(), token, {\n httpOnly: true,\n // secure must be false on HTTP dev (next dev on localhost) so the browser stores\n // the cookie. In production builds (next start / Vercel) this becomes true.\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n expires: new Date(exp),\n });\n\n // Return the target instead of calling redirect() here. The client-side\n // <StageLogin> reads this in a useEffect and calls router.replace, which\n // guarantees the Set-Cookie has been committed to the browser jar before\n // the next GET fires. This eliminates the cookie-commit vs redirect race\n // that surfaces on Vercel's edge runtime + CDN.\n return { redirectTo: safeRedirectTarget(formData.get('from')) };\n}\n","// Internal helpers for the stage-environment auth gate.\n// Edge-runtime safe (uses Web Crypto, no Node APIs).\n\nconst COOKIE_NAME = 'olmo_stage_auth';\nconst DEFAULT_SESSION_DAYS = 7;\n\nexport type StagePayload = { u: string; exp: number };\n\nexport type StageEnv = {\n username: string;\n password: string;\n secret: string;\n sessionDays: number;\n};\n\nexport function readStageEnv(): StageEnv | null {\n const username = process.env.OLMO_STAGE_USERNAME;\n const password = process.env.OLMO_STAGE_PASSWORD;\n const secret = process.env.OLMO_STAGE_SECRET;\n if (!username || !password || !secret) return null;\n const days = Number(process.env.OLMO_STAGE_SESSION_DAYS);\n return {\n username,\n password,\n secret,\n sessionDays: Number.isFinite(days) && days > 0 ? days : DEFAULT_SESSION_DAYS,\n };\n}\n\nexport function getStageCookieName(): string {\n return COOKIE_NAME;\n}\n\nfunction toBase64Url(bytes: ArrayBuffer | Uint8Array): string {\n const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n let str = '';\n for (let i = 0; i < arr.length; i++) str += String.fromCharCode(arr[i]);\n return btoa(str).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction fromBase64Url(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? '' : '='.repeat(4 - (input.length % 4));\n const b64 = input.replace(/-/g, '+').replace(/_/g, '/') + pad;\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\nasync function importKey(secret: string): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign', 'verify'],\n );\n}\n\nexport async function signSession(payload: StagePayload, secret: string): Promise<string> {\n const body = toBase64Url(new TextEncoder().encode(JSON.stringify(payload)));\n const key = await importKey(secret);\n const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body));\n return `${body}.${toBase64Url(sig)}`;\n}\n\nexport async function verifySession(token: string | undefined, secret: string): Promise<StagePayload | null> {\n if (!token || typeof token !== 'string' || token.indexOf('.') === -1) return null;\n const [body, sig] = token.split('.');\n if (!body || !sig) return null;\n let key: CryptoKey;\n try {\n key = await importKey(secret);\n } catch {\n return null;\n }\n let valid = false;\n try {\n valid = await crypto.subtle.verify('HMAC', key, fromBase64Url(sig) as BufferSource, new TextEncoder().encode(body));\n } catch {\n return null;\n }\n if (!valid) return null;\n let payload: StagePayload;\n try {\n payload = JSON.parse(new TextDecoder().decode(fromBase64Url(body)));\n } catch {\n return null;\n }\n if (!payload || typeof payload.exp !== 'number' || payload.exp < Date.now()) return null;\n return payload;\n}\n\nexport function verifyCredentials(env: StageEnv, username: unknown, password: unknown): boolean {\n if (typeof username !== 'string' || typeof password !== 'string') return false;\n return username === env.username && password === env.password;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,qBAAwB;;;ACCxB,IAAM,cAAc;AACpB,IAAM,uBAAuB;AAWtB,SAAS,eAAgC;AAC9C,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,YAAY,CAAC,YAAY,CAAC,OAAQ,QAAO;AAC9C,QAAM,OAAO,OAAO,QAAQ,IAAI,uBAAuB;AACvD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI,OAAO;AAAA,EAC1D;AACF;AAEO,SAAS,qBAA6B;AAC3C,SAAO;AACT;AAEA,SAAS,YAAY,OAAyC;AAC5D,QAAM,MAAM,iBAAiB,aAAa,QAAQ,IAAI,WAAW,KAAK;AACtE,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,QAAO,OAAO,aAAa,IAAI,CAAC,CAAC;AACtE,SAAO,KAAK,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC5E;AAWA,eAAe,UAAU,QAAoC;AAC3D,SAAO,OAAO,OAAO;AAAA,IACnB;AAAA,IACA,IAAI,YAAY,EAAE,OAAO,MAAM;AAAA,IAC/B,EAAE,MAAM,QAAQ,MAAM,UAAU;AAAA,IAChC;AAAA,IACA,CAAC,QAAQ,QAAQ;AAAA,EACnB;AACF;AAEA,eAAsB,YAAY,SAAuB,QAAiC;AACxF,QAAM,OAAO,YAAY,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,OAAO,CAAC,CAAC;AAC1E,QAAM,MAAM,MAAM,UAAU,MAAM;AAClC,QAAM,MAAM,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,YAAY,EAAE,OAAO,IAAI,CAAC;AAChF,SAAO,GAAG,IAAI,IAAI,YAAY,GAAG,CAAC;AACpC;AA6BO,SAAS,kBAAkB,KAAe,UAAmB,UAA4B;AAC9F,MAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SAAU,QAAO;AACzE,SAAO,aAAa,IAAI,YAAY,aAAa,IAAI;AACvD;;;ADtFA,SAAS,mBAAmB,KAAsB;AAChD,MAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAAG,QAAO;AACxD,MAAI,CAAC,IAAI,WAAW,GAAG,KAAK,IAAI,WAAW,IAAI,EAAG,QAAO;AACzD,SAAO;AACT;AAEA,eAAsB,iBAAiB,OAAwB,UAA8C;AAC3G,QAAM,MAAM,aAAa;AACzB,MAAI,CAAC,IAAK,QAAO,EAAE,OAAO,iBAAiB;AAE3C,QAAM,WAAW,SAAS,IAAI,UAAU;AACxC,QAAM,WAAW,SAAS,IAAI,UAAU;AACxC,MAAI,CAAC,kBAAkB,KAAK,UAAU,QAAQ,GAAG;AAC/C,WAAO,EAAE,OAAO,sBAAsB;AAAA,EACxC;AAEA,QAAM,MAAM,KAAK,IAAI,IAAI,IAAI,cAAc,KAAK,KAAK,KAAK;AAC1D,QAAM,QAAQ,MAAM,YAAY,EAAE,GAAG,IAAI,UAAU,IAAI,GAAG,IAAI,MAAM;AAEpE,QAAM,QAAQ,UAAM,wBAAQ;AAC5B,QAAM,IAAI,mBAAmB,GAAG,OAAO;AAAA,IACrC,UAAU;AAAA;AAAA;AAAA,IAGV,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,UAAU;AAAA,IACV,MAAM;AAAA,IACN,SAAS,IAAI,KAAK,GAAG;AAAA,EACvB,CAAC;AAOD,SAAO,EAAE,YAAY,mBAAmB,SAAS,IAAI,MAAM,CAAC,EAAE;AAChE;","names":[]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type StageLoginState = {
|
|
2
|
+
error?: 'missing_config' | 'invalid_credentials' | null;
|
|
3
|
+
redirectTo?: string | null;
|
|
4
|
+
};
|
|
5
|
+
declare function stageLoginAction(_prev: StageLoginState, formData: FormData): Promise<StageLoginState>;
|
|
6
|
+
|
|
7
|
+
export { type StageLoginState, stageLoginAction };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type StageLoginState = {
|
|
2
|
+
error?: 'missing_config' | 'invalid_credentials' | null;
|
|
3
|
+
redirectTo?: string | null;
|
|
4
|
+
};
|
|
5
|
+
declare function stageLoginAction(_prev: StageLoginState, formData: FormData): Promise<StageLoginState>;
|
|
6
|
+
|
|
7
|
+
export { type StageLoginState, stageLoginAction };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
// src/auth/action.ts
|
|
4
|
+
import { cookies } from "next/headers";
|
|
5
|
+
|
|
6
|
+
// src/auth/stage.ts
|
|
7
|
+
var COOKIE_NAME = "olmo_stage_auth";
|
|
8
|
+
var DEFAULT_SESSION_DAYS = 7;
|
|
9
|
+
function readStageEnv() {
|
|
10
|
+
const username = process.env.OLMO_STAGE_USERNAME;
|
|
11
|
+
const password = process.env.OLMO_STAGE_PASSWORD;
|
|
12
|
+
const secret = process.env.OLMO_STAGE_SECRET;
|
|
13
|
+
if (!username || !password || !secret) return null;
|
|
14
|
+
const days = Number(process.env.OLMO_STAGE_SESSION_DAYS);
|
|
15
|
+
return {
|
|
16
|
+
username,
|
|
17
|
+
password,
|
|
18
|
+
secret,
|
|
19
|
+
sessionDays: Number.isFinite(days) && days > 0 ? days : DEFAULT_SESSION_DAYS
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function getStageCookieName() {
|
|
23
|
+
return COOKIE_NAME;
|
|
24
|
+
}
|
|
25
|
+
function toBase64Url(bytes) {
|
|
26
|
+
const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
27
|
+
let str = "";
|
|
28
|
+
for (let i = 0; i < arr.length; i++) str += String.fromCharCode(arr[i]);
|
|
29
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
30
|
+
}
|
|
31
|
+
async function importKey(secret) {
|
|
32
|
+
return crypto.subtle.importKey(
|
|
33
|
+
"raw",
|
|
34
|
+
new TextEncoder().encode(secret),
|
|
35
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
36
|
+
false,
|
|
37
|
+
["sign", "verify"]
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
async function signSession(payload, secret) {
|
|
41
|
+
const body = toBase64Url(new TextEncoder().encode(JSON.stringify(payload)));
|
|
42
|
+
const key = await importKey(secret);
|
|
43
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body));
|
|
44
|
+
return `${body}.${toBase64Url(sig)}`;
|
|
45
|
+
}
|
|
46
|
+
function verifyCredentials(env, username, password) {
|
|
47
|
+
if (typeof username !== "string" || typeof password !== "string") return false;
|
|
48
|
+
return username === env.username && password === env.password;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/auth/action.ts
|
|
52
|
+
function safeRedirectTarget(raw) {
|
|
53
|
+
if (typeof raw !== "string" || raw.length === 0) return "/";
|
|
54
|
+
if (!raw.startsWith("/") || raw.startsWith("//")) return "/";
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
async function stageLoginAction(_prev, formData) {
|
|
58
|
+
const env = readStageEnv();
|
|
59
|
+
if (!env) return { error: "missing_config" };
|
|
60
|
+
const username = formData.get("username");
|
|
61
|
+
const password = formData.get("password");
|
|
62
|
+
if (!verifyCredentials(env, username, password)) {
|
|
63
|
+
return { error: "invalid_credentials" };
|
|
64
|
+
}
|
|
65
|
+
const exp = Date.now() + env.sessionDays * 24 * 60 * 60 * 1e3;
|
|
66
|
+
const token = await signSession({ u: env.username, exp }, env.secret);
|
|
67
|
+
const store = await cookies();
|
|
68
|
+
store.set(getStageCookieName(), token, {
|
|
69
|
+
httpOnly: true,
|
|
70
|
+
// secure must be false on HTTP dev (next dev on localhost) so the browser stores
|
|
71
|
+
// the cookie. In production builds (next start / Vercel) this becomes true.
|
|
72
|
+
secure: process.env.NODE_ENV === "production",
|
|
73
|
+
sameSite: "lax",
|
|
74
|
+
path: "/",
|
|
75
|
+
expires: new Date(exp)
|
|
76
|
+
});
|
|
77
|
+
return { redirectTo: safeRedirectTarget(formData.get("from")) };
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
stageLoginAction
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=action.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/auth/action.ts","../../src/auth/stage.ts"],"sourcesContent":["// 'use server' is injected by the tsup banner — keep this file directive-free\n// so the bundled output has exactly one directive at the top.\nimport { cookies } from 'next/headers';\nimport { getStageCookieName, readStageEnv, signSession, verifyCredentials } from './stage.js';\n\nexport type StageLoginState = {\n error?: 'missing_config' | 'invalid_credentials' | null;\n redirectTo?: string | null;\n};\n\nfunction safeRedirectTarget(raw: unknown): string {\n if (typeof raw !== 'string' || raw.length === 0) return '/';\n if (!raw.startsWith('/') || raw.startsWith('//')) return '/';\n return raw;\n}\n\nexport async function stageLoginAction(_prev: StageLoginState, formData: FormData): Promise<StageLoginState> {\n const env = readStageEnv();\n if (!env) return { error: 'missing_config' };\n\n const username = formData.get('username');\n const password = formData.get('password');\n if (!verifyCredentials(env, username, password)) {\n return { error: 'invalid_credentials' };\n }\n\n const exp = Date.now() + env.sessionDays * 24 * 60 * 60 * 1000;\n const token = await signSession({ u: env.username, exp }, env.secret);\n\n const store = await cookies();\n store.set(getStageCookieName(), token, {\n httpOnly: true,\n // secure must be false on HTTP dev (next dev on localhost) so the browser stores\n // the cookie. In production builds (next start / Vercel) this becomes true.\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n expires: new Date(exp),\n });\n\n // Return the target instead of calling redirect() here. The client-side\n // <StageLogin> reads this in a useEffect and calls router.replace, which\n // guarantees the Set-Cookie has been committed to the browser jar before\n // the next GET fires. This eliminates the cookie-commit vs redirect race\n // that surfaces on Vercel's edge runtime + CDN.\n return { redirectTo: safeRedirectTarget(formData.get('from')) };\n}\n","// Internal helpers for the stage-environment auth gate.\n// Edge-runtime safe (uses Web Crypto, no Node APIs).\n\nconst COOKIE_NAME = 'olmo_stage_auth';\nconst DEFAULT_SESSION_DAYS = 7;\n\nexport type StagePayload = { u: string; exp: number };\n\nexport type StageEnv = {\n username: string;\n password: string;\n secret: string;\n sessionDays: number;\n};\n\nexport function readStageEnv(): StageEnv | null {\n const username = process.env.OLMO_STAGE_USERNAME;\n const password = process.env.OLMO_STAGE_PASSWORD;\n const secret = process.env.OLMO_STAGE_SECRET;\n if (!username || !password || !secret) return null;\n const days = Number(process.env.OLMO_STAGE_SESSION_DAYS);\n return {\n username,\n password,\n secret,\n sessionDays: Number.isFinite(days) && days > 0 ? days : DEFAULT_SESSION_DAYS,\n };\n}\n\nexport function getStageCookieName(): string {\n return COOKIE_NAME;\n}\n\nfunction toBase64Url(bytes: ArrayBuffer | Uint8Array): string {\n const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n let str = '';\n for (let i = 0; i < arr.length; i++) str += String.fromCharCode(arr[i]);\n return btoa(str).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction fromBase64Url(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? '' : '='.repeat(4 - (input.length % 4));\n const b64 = input.replace(/-/g, '+').replace(/_/g, '/') + pad;\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\nasync function importKey(secret: string): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign', 'verify'],\n );\n}\n\nexport async function signSession(payload: StagePayload, secret: string): Promise<string> {\n const body = toBase64Url(new TextEncoder().encode(JSON.stringify(payload)));\n const key = await importKey(secret);\n const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body));\n return `${body}.${toBase64Url(sig)}`;\n}\n\nexport async function verifySession(token: string | undefined, secret: string): Promise<StagePayload | null> {\n if (!token || typeof token !== 'string' || token.indexOf('.') === -1) return null;\n const [body, sig] = token.split('.');\n if (!body || !sig) return null;\n let key: CryptoKey;\n try {\n key = await importKey(secret);\n } catch {\n return null;\n }\n let valid = false;\n try {\n valid = await crypto.subtle.verify('HMAC', key, fromBase64Url(sig) as BufferSource, new TextEncoder().encode(body));\n } catch {\n return null;\n }\n if (!valid) return null;\n let payload: StagePayload;\n try {\n payload = JSON.parse(new TextDecoder().decode(fromBase64Url(body)));\n } catch {\n return null;\n }\n if (!payload || typeof payload.exp !== 'number' || payload.exp < Date.now()) return null;\n return payload;\n}\n\nexport function verifyCredentials(env: StageEnv, username: unknown, password: unknown): boolean {\n if (typeof username !== 'string' || typeof password !== 'string') return false;\n return username === env.username && password === env.password;\n}\n"],"mappings":";;;AAEA,SAAS,eAAe;;;ACCxB,IAAM,cAAc;AACpB,IAAM,uBAAuB;AAWtB,SAAS,eAAgC;AAC9C,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,YAAY,CAAC,YAAY,CAAC,OAAQ,QAAO;AAC9C,QAAM,OAAO,OAAO,QAAQ,IAAI,uBAAuB;AACvD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI,OAAO;AAAA,EAC1D;AACF;AAEO,SAAS,qBAA6B;AAC3C,SAAO;AACT;AAEA,SAAS,YAAY,OAAyC;AAC5D,QAAM,MAAM,iBAAiB,aAAa,QAAQ,IAAI,WAAW,KAAK;AACtE,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,QAAO,OAAO,aAAa,IAAI,CAAC,CAAC;AACtE,SAAO,KAAK,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC5E;AAWA,eAAe,UAAU,QAAoC;AAC3D,SAAO,OAAO,OAAO;AAAA,IACnB;AAAA,IACA,IAAI,YAAY,EAAE,OAAO,MAAM;AAAA,IAC/B,EAAE,MAAM,QAAQ,MAAM,UAAU;AAAA,IAChC;AAAA,IACA,CAAC,QAAQ,QAAQ;AAAA,EACnB;AACF;AAEA,eAAsB,YAAY,SAAuB,QAAiC;AACxF,QAAM,OAAO,YAAY,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,OAAO,CAAC,CAAC;AAC1E,QAAM,MAAM,MAAM,UAAU,MAAM;AAClC,QAAM,MAAM,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,YAAY,EAAE,OAAO,IAAI,CAAC;AAChF,SAAO,GAAG,IAAI,IAAI,YAAY,GAAG,CAAC;AACpC;AA6BO,SAAS,kBAAkB,KAAe,UAAmB,UAA4B;AAC9F,MAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SAAU,QAAO;AACzE,SAAO,aAAa,IAAI,YAAY,aAAa,IAAI;AACvD;;;ADtFA,SAAS,mBAAmB,KAAsB;AAChD,MAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAAG,QAAO;AACxD,MAAI,CAAC,IAAI,WAAW,GAAG,KAAK,IAAI,WAAW,IAAI,EAAG,QAAO;AACzD,SAAO;AACT;AAEA,eAAsB,iBAAiB,OAAwB,UAA8C;AAC3G,QAAM,MAAM,aAAa;AACzB,MAAI,CAAC,IAAK,QAAO,EAAE,OAAO,iBAAiB;AAE3C,QAAM,WAAW,SAAS,IAAI,UAAU;AACxC,QAAM,WAAW,SAAS,IAAI,UAAU;AACxC,MAAI,CAAC,kBAAkB,KAAK,UAAU,QAAQ,GAAG;AAC/C,WAAO,EAAE,OAAO,sBAAsB;AAAA,EACxC;AAEA,QAAM,MAAM,KAAK,IAAI,IAAI,IAAI,cAAc,KAAK,KAAK,KAAK;AAC1D,QAAM,QAAQ,MAAM,YAAY,EAAE,GAAG,IAAI,UAAU,IAAI,GAAG,IAAI,MAAM;AAEpE,QAAM,QAAQ,MAAM,QAAQ;AAC5B,QAAM,IAAI,mBAAmB,GAAG,OAAO;AAAA,IACrC,UAAU;AAAA;AAAA;AAAA,IAGV,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,UAAU;AAAA,IACV,MAAM;AAAA,IACN,SAAS,IAAI,KAAK,GAAG;AAAA,EACvB,CAAC;AAOD,SAAO,EAAE,YAAY,mBAAmB,SAAS,IAAI,MAAM,CAAC,EAAE;AAChE;","names":[]}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/middleware/chain.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
function chain(functions = [], index = 0) {
|
|
4
|
+
const current = functions[index];
|
|
5
|
+
if (current) {
|
|
6
|
+
const next = chain(functions, index + 1);
|
|
7
|
+
return current(next);
|
|
8
|
+
}
|
|
9
|
+
return () => NextResponse.next();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/middleware/headermiddleware.ts
|
|
13
|
+
import { NextRequest } from "next/server";
|
|
14
|
+
var headermiddleware = (next) => {
|
|
15
|
+
return async (request, _next) => {
|
|
16
|
+
const url = new URL(request.url);
|
|
17
|
+
const params = new URLSearchParams(url.search);
|
|
18
|
+
const headers = new Headers(request.headers);
|
|
19
|
+
headers.set("x-current-path", request.nextUrl.pathname);
|
|
20
|
+
headers.set("x-server", "true");
|
|
21
|
+
headers.set("olmo-preview", params.has("olmopreview").toString());
|
|
22
|
+
return next(new NextRequest(request.url, { headers }), _next);
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/middleware/redirectmiddleware.ts
|
|
27
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
28
|
+
var redirectmiddleware = (next) => {
|
|
29
|
+
return async (request, _next) => {
|
|
30
|
+
if (request.headers.has("rsc")) {
|
|
31
|
+
return next(request, _next);
|
|
32
|
+
}
|
|
33
|
+
if (!process.env.NEXT_PUBLIC_OLMO_TOKEN || !process.env.NEXT_PUBLIC_API_URL || !process.env.NEXT_PUBLIC_BASE_URL) {
|
|
34
|
+
return next(request, _next);
|
|
35
|
+
}
|
|
36
|
+
const pathNameWithTrailingSlash = request.nextUrl.pathname;
|
|
37
|
+
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/all/redirect`, {
|
|
38
|
+
method: "GET",
|
|
39
|
+
cache: "force-cache",
|
|
40
|
+
next: { tags: ["olmo", "redirect"] },
|
|
41
|
+
headers: { "front-token": process.env.NEXT_PUBLIC_OLMO_TOKEN }
|
|
42
|
+
});
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
const redirects = data.map((e) => ({
|
|
45
|
+
source: e.source,
|
|
46
|
+
destination: e.destination,
|
|
47
|
+
permanent: e.permanent === "true"
|
|
48
|
+
}));
|
|
49
|
+
if (redirects.length > 0) {
|
|
50
|
+
const redirect = redirects.find((item) => item.source === pathNameWithTrailingSlash);
|
|
51
|
+
if (!redirect) {
|
|
52
|
+
return next(request, _next);
|
|
53
|
+
}
|
|
54
|
+
const newUrl = new URL(redirect.destination, process.env.NEXT_PUBLIC_BASE_URL).toString();
|
|
55
|
+
return NextResponse2.redirect(newUrl, { status: redirect.permanent ? 308 : 307 });
|
|
56
|
+
}
|
|
57
|
+
return next(request, _next);
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/middleware/stagegatemiddleware.ts
|
|
62
|
+
import { NextResponse as NextResponse3 } from "next/server";
|
|
63
|
+
|
|
64
|
+
// src/auth/stage.ts
|
|
65
|
+
var COOKIE_NAME = "olmo_stage_auth";
|
|
66
|
+
var DEFAULT_SESSION_DAYS = 7;
|
|
67
|
+
function readStageEnv() {
|
|
68
|
+
const username = process.env.OLMO_STAGE_USERNAME;
|
|
69
|
+
const password = process.env.OLMO_STAGE_PASSWORD;
|
|
70
|
+
const secret = process.env.OLMO_STAGE_SECRET;
|
|
71
|
+
if (!username || !password || !secret) return null;
|
|
72
|
+
const days = Number(process.env.OLMO_STAGE_SESSION_DAYS);
|
|
73
|
+
return {
|
|
74
|
+
username,
|
|
75
|
+
password,
|
|
76
|
+
secret,
|
|
77
|
+
sessionDays: Number.isFinite(days) && days > 0 ? days : DEFAULT_SESSION_DAYS
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function getStageCookieName() {
|
|
81
|
+
return COOKIE_NAME;
|
|
82
|
+
}
|
|
83
|
+
function fromBase64Url(input) {
|
|
84
|
+
const pad = input.length % 4 === 0 ? "" : "=".repeat(4 - input.length % 4);
|
|
85
|
+
const b64 = input.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
86
|
+
const bin = atob(b64);
|
|
87
|
+
const out = new Uint8Array(bin.length);
|
|
88
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
async function importKey(secret) {
|
|
92
|
+
return crypto.subtle.importKey(
|
|
93
|
+
"raw",
|
|
94
|
+
new TextEncoder().encode(secret),
|
|
95
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
96
|
+
false,
|
|
97
|
+
["sign", "verify"]
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
async function verifySession(token, secret) {
|
|
101
|
+
if (!token || typeof token !== "string" || token.indexOf(".") === -1) return null;
|
|
102
|
+
const [body, sig] = token.split(".");
|
|
103
|
+
if (!body || !sig) return null;
|
|
104
|
+
let key;
|
|
105
|
+
try {
|
|
106
|
+
key = await importKey(secret);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
let valid = false;
|
|
111
|
+
try {
|
|
112
|
+
valid = await crypto.subtle.verify("HMAC", key, fromBase64Url(sig), new TextEncoder().encode(body));
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (!valid) return null;
|
|
117
|
+
let payload;
|
|
118
|
+
try {
|
|
119
|
+
payload = JSON.parse(new TextDecoder().decode(fromBase64Url(body)));
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (!payload || typeof payload.exp !== "number" || payload.exp < Date.now()) return null;
|
|
124
|
+
return payload;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/middleware/stagegatemiddleware.ts
|
|
128
|
+
var LOGIN_PATH = "/olmo-stage-login/";
|
|
129
|
+
var NOINDEX = "noindex, nofollow, noarchive";
|
|
130
|
+
function withNoindex(res) {
|
|
131
|
+
res.headers.set("X-Robots-Tag", NOINDEX);
|
|
132
|
+
return res;
|
|
133
|
+
}
|
|
134
|
+
var stagegatemiddleware = (next) => {
|
|
135
|
+
return async (request, _next) => {
|
|
136
|
+
if (process.env.NEXT_PUBLIC_ENV !== "stage") {
|
|
137
|
+
return next(request, _next);
|
|
138
|
+
}
|
|
139
|
+
const { pathname, search } = request.nextUrl;
|
|
140
|
+
if (pathname === LOGIN_PATH) {
|
|
141
|
+
return withNoindex(NextResponse3.next());
|
|
142
|
+
}
|
|
143
|
+
const env = readStageEnv();
|
|
144
|
+
if (!env) {
|
|
145
|
+
const url2 = request.nextUrl.clone();
|
|
146
|
+
url2.pathname = LOGIN_PATH;
|
|
147
|
+
url2.search = "";
|
|
148
|
+
return withNoindex(NextResponse3.redirect(url2, { status: 307 }));
|
|
149
|
+
}
|
|
150
|
+
const token = request.cookies.get(getStageCookieName())?.value;
|
|
151
|
+
const session = await verifySession(token, env.secret);
|
|
152
|
+
if (session) {
|
|
153
|
+
const res = await next(request, _next);
|
|
154
|
+
return withNoindex(res instanceof NextResponse3 ? res : NextResponse3.next());
|
|
155
|
+
}
|
|
156
|
+
const url = request.nextUrl.clone();
|
|
157
|
+
url.pathname = LOGIN_PATH;
|
|
158
|
+
url.search = `?from=${encodeURIComponent(pathname + search)}`;
|
|
159
|
+
return withNoindex(NextResponse3.redirect(url, { status: 307 }));
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
chain,
|
|
165
|
+
headermiddleware,
|
|
166
|
+
redirectmiddleware,
|
|
167
|
+
stagegatemiddleware
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=chunk-UCR7WW3X.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware/chain.ts","../src/middleware/headermiddleware.ts","../src/middleware/redirectmiddleware.ts","../src/middleware/stagegatemiddleware.ts","../src/auth/stage.ts"],"sourcesContent":["import { NextResponse } from 'next/server';\nimport type { NextMiddleware } from 'next/server';\n\nexport type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;\n\nexport function chain(functions: MiddlewareFactory[] = [], index = 0): NextMiddleware {\n const current = functions[index];\n if (current) {\n const next = chain(functions, index + 1);\n return current(next);\n }\n return () => NextResponse.next();\n}\n","import { NextRequest } from 'next/server';\nimport type { NextFetchEvent } from 'next/server';\nimport type { MiddlewareFactory } from './chain.js';\n\nexport const headermiddleware: MiddlewareFactory = (next) => {\n return async (request: NextRequest, _next: NextFetchEvent) => {\n const url = new URL(request.url);\n const params = new URLSearchParams(url.search);\n\n const headers = new Headers(request.headers);\n headers.set('x-current-path', request.nextUrl.pathname);\n headers.set('x-server', 'true');\n headers.set('olmo-preview', params.has('olmopreview').toString());\n\n return next(new NextRequest(request.url, { headers }), _next);\n };\n};\n","import { NextResponse } from 'next/server';\nimport type { NextFetchEvent, NextRequest } from 'next/server';\nimport type { MiddlewareFactory } from './chain.js';\n\ntype NextFetchRequestInit = RequestInit & { next?: { tags?: string[]; revalidate?: number | false } };\n\nexport const redirectmiddleware: MiddlewareFactory = (next) => {\n return async (request: NextRequest, _next: NextFetchEvent) => {\n if (request.headers.has('rsc')) {\n return next(request, _next);\n }\n\n if (!process.env.NEXT_PUBLIC_OLMO_TOKEN || !process.env.NEXT_PUBLIC_API_URL || !process.env.NEXT_PUBLIC_BASE_URL) {\n return next(request, _next);\n }\n\n const pathNameWithTrailingSlash = request.nextUrl.pathname;\n\n const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/all/redirect`, {\n method: 'GET',\n cache: 'force-cache',\n next: { tags: ['olmo', 'redirect'] },\n headers: { 'front-token': process.env.NEXT_PUBLIC_OLMO_TOKEN },\n } as NextFetchRequestInit);\n\n const data = await response.json();\n\n const redirects = data.map((e: any) => ({\n source: e.source,\n destination: e.destination,\n permanent: e.permanent === 'true',\n }));\n\n if (redirects.length > 0) {\n const redirect = redirects.find((item: any) => item.source === pathNameWithTrailingSlash);\n\n if (!redirect) {\n return next(request, _next);\n }\n\n const newUrl = new URL(redirect.destination, process.env.NEXT_PUBLIC_BASE_URL).toString();\n return NextResponse.redirect(newUrl, { status: redirect.permanent ? 308 : 307 });\n }\n\n return next(request, _next);\n };\n};\n","import { NextResponse } from 'next/server';\nimport type { NextFetchEvent, NextRequest } from 'next/server';\nimport { getStageCookieName, readStageEnv, verifySession } from '../auth/stage.js';\nimport type { MiddlewareFactory } from './chain.js';\n\nconst LOGIN_PATH = '/olmo-stage-login/';\nconst NOINDEX = 'noindex, nofollow, noarchive';\n\nfunction withNoindex(res: NextResponse): NextResponse {\n res.headers.set('X-Robots-Tag', NOINDEX);\n return res;\n}\n\nexport const stagegatemiddleware: MiddlewareFactory = (next) => {\n return async (request: NextRequest, _next: NextFetchEvent) => {\n if (process.env.NEXT_PUBLIC_ENV !== 'stage') {\n return next(request, _next);\n }\n\n const { pathname, search } = request.nextUrl;\n\n if (pathname === LOGIN_PATH) {\n return withNoindex(NextResponse.next());\n }\n\n const env = readStageEnv();\n if (!env) {\n // Misconfigured stage — deny everything except the login page itself.\n const url = request.nextUrl.clone();\n url.pathname = LOGIN_PATH;\n url.search = '';\n return withNoindex(NextResponse.redirect(url, { status: 307 }));\n }\n\n const token = request.cookies.get(getStageCookieName())?.value;\n const session = await verifySession(token, env.secret);\n if (session) {\n const res = await next(request, _next);\n return withNoindex(res instanceof NextResponse ? res : NextResponse.next());\n }\n\n const url = request.nextUrl.clone();\n url.pathname = LOGIN_PATH;\n url.search = `?from=${encodeURIComponent(pathname + search)}`;\n return withNoindex(NextResponse.redirect(url, { status: 307 }));\n };\n};\n","// Internal helpers for the stage-environment auth gate.\n// Edge-runtime safe (uses Web Crypto, no Node APIs).\n\nconst COOKIE_NAME = 'olmo_stage_auth';\nconst DEFAULT_SESSION_DAYS = 7;\n\nexport type StagePayload = { u: string; exp: number };\n\nexport type StageEnv = {\n username: string;\n password: string;\n secret: string;\n sessionDays: number;\n};\n\nexport function readStageEnv(): StageEnv | null {\n const username = process.env.OLMO_STAGE_USERNAME;\n const password = process.env.OLMO_STAGE_PASSWORD;\n const secret = process.env.OLMO_STAGE_SECRET;\n if (!username || !password || !secret) return null;\n const days = Number(process.env.OLMO_STAGE_SESSION_DAYS);\n return {\n username,\n password,\n secret,\n sessionDays: Number.isFinite(days) && days > 0 ? days : DEFAULT_SESSION_DAYS,\n };\n}\n\nexport function getStageCookieName(): string {\n return COOKIE_NAME;\n}\n\nfunction toBase64Url(bytes: ArrayBuffer | Uint8Array): string {\n const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n let str = '';\n for (let i = 0; i < arr.length; i++) str += String.fromCharCode(arr[i]);\n return btoa(str).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction fromBase64Url(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? '' : '='.repeat(4 - (input.length % 4));\n const b64 = input.replace(/-/g, '+').replace(/_/g, '/') + pad;\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\nasync function importKey(secret: string): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign', 'verify'],\n );\n}\n\nexport async function signSession(payload: StagePayload, secret: string): Promise<string> {\n const body = toBase64Url(new TextEncoder().encode(JSON.stringify(payload)));\n const key = await importKey(secret);\n const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body));\n return `${body}.${toBase64Url(sig)}`;\n}\n\nexport async function verifySession(token: string | undefined, secret: string): Promise<StagePayload | null> {\n if (!token || typeof token !== 'string' || token.indexOf('.') === -1) return null;\n const [body, sig] = token.split('.');\n if (!body || !sig) return null;\n let key: CryptoKey;\n try {\n key = await importKey(secret);\n } catch {\n return null;\n }\n let valid = false;\n try {\n valid = await crypto.subtle.verify('HMAC', key, fromBase64Url(sig) as BufferSource, new TextEncoder().encode(body));\n } catch {\n return null;\n }\n if (!valid) return null;\n let payload: StagePayload;\n try {\n payload = JSON.parse(new TextDecoder().decode(fromBase64Url(body)));\n } catch {\n return null;\n }\n if (!payload || typeof payload.exp !== 'number' || payload.exp < Date.now()) return null;\n return payload;\n}\n\nexport function verifyCredentials(env: StageEnv, username: unknown, password: unknown): boolean {\n if (typeof username !== 'string' || typeof password !== 'string') return false;\n return username === env.username && password === env.password;\n}\n"],"mappings":"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","names":["NextResponse","NextResponse","NextResponse","url"]}
|
package/dist/cli/index.js
CHANGED
|
@@ -250,7 +250,7 @@ var addRouteCommand = new Command("route").description("Scaffold a new Olmo page
|
|
|
250
250
|
{ title: "List page (fetches a collection)", value: "list" },
|
|
251
251
|
{ title: "Single/detail page (slug route)", value: "single" }
|
|
252
252
|
],
|
|
253
|
-
initial: defaultType === "
|
|
253
|
+
initial: defaultType === "single" ? 2 : 0
|
|
254
254
|
});
|
|
255
255
|
if (!res.value) process.exit(0);
|
|
256
256
|
routeType = res.value;
|