orionplace-react 0.1.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 +242 -0
- package/bin/orionplace-react.mjs +101 -0
- package/package.json +56 -0
- package/runtime/marketplace.d.mts +22 -0
- package/runtime/marketplace.mjs +211 -0
- package/src/project-readme.mjs +341 -0
- package/src/setup.mjs +250 -0
- package/src/update.mjs +36 -0
- package/src/validation.mjs +27 -0
- package/templates/app/api/[...orionplace]/route.ts +14 -0
- package/templates/app/globals.css +10 -0
- package/templates/app/layout.tsx +25 -0
- package/templates/app/page.module.css +133 -0
- package/templates/app/page.tsx +80 -0
- package/templates/hooks/useOrionplaceAppLoad.ts +34 -0
- package/templates/interfaces/orionplace/apiBase/TMethod.ts +1 -0
- package/templates/interfaces/orionplace/appBase/IAppLoad.ts +15 -0
- package/templates/interfaces/orionplace/appBase/IAppLoadCommon.ts +54 -0
- package/templates/interfaces/orionplace/appBase/IApploadUserProfile.ts +13 -0
- package/templates/interfaces/orionplace/appBase/IForm.ts +21 -0
- package/templates/interfaces/orionplace/appBase/ISEO.ts +21 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonBanner.ts +11 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonContact.ts +8 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonCurrency.ts +15 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonEvents.ts +18 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLocale.ts +14 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLogos.ts +6 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonPath.ts +22 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSEO.ts +13 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSocial.ts +15 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonTariff.ts +7 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonUnit.ts +12 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVariables.ts +7 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVisualSettings.ts +11 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonWalletValue.ts +3 -0
- package/templates/interfaces/orionplace/category/ICategory.ts +40 -0
- package/templates/interfaces/orionplace/domains/IDomain.ts +13 -0
- package/templates/interfaces/orionplace/index.ts +30 -0
- package/templates/interfaces/orionplace/products/IProduct.ts +235 -0
- package/templates/interfaces/orionplace/shared/IGeoCoordinates.ts +4 -0
- package/templates/interfaces/orionplace/shared/ITransformValue.ts +3 -0
- package/templates/interfaces/orionplace/state.ts +4 -0
- package/templates/interfaces/orionplace/subscribeOptions/ISubscribeOption.ts +9 -0
- package/templates/interfaces/orionplace/subscribeTariff/ISubscribeTariff.ts +46 -0
- package/templates/interfaces/orionplace/subscribes/ISubscribe.ts +28 -0
- package/templates/interfaces/orionplace/transport.ts +30 -0
- package/templates/lib/orionplace/appLoadClient.ts +33 -0
- package/templates/lib/orionplace/appLoadResult.ts +56 -0
- package/templates/lib/orionplace/appLoadServer.ts +72 -0
- package/templates/lib/orionplace/domain.ts +16 -0
- package/templates/providers/OrionplaceProvider.tsx +39 -0
- package/templates/proxy.ts +17 -0
- package/templates/stores/orionplace.ts +119 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orionplace 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
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# orionplace-react
|
|
2
|
+
|
|
3
|
+
Set up a Next.js 16 storefront for your personal Orionplace marketplace API.
|
|
4
|
+
The package includes a setup CLI, the App Router starter with typed AppLoad
|
|
5
|
+
initialization, and commented server source that is copied into your project
|
|
6
|
+
alongside an integration README.
|
|
7
|
+
|
|
8
|
+
Requires Node.js 20.9+ and a Node server deployment. Static export and edge-only
|
|
9
|
+
hosting are not supported by this filesystem-based integration.
|
|
10
|
+
|
|
11
|
+
## Quick start from zero
|
|
12
|
+
|
|
13
|
+
Your personal backend domain is already provided in your account at
|
|
14
|
+
[orionmarket.place](https://orionmarket.place). You can change it there.
|
|
15
|
+
Replace `your-project.orionplace.app` below with that domain and
|
|
16
|
+
`shop.example.com` with the public domain of your own storefront.
|
|
17
|
+
With Node.js 20.9+ and npm installed, run this from the parent folder;
|
|
18
|
+
no existing Next.js project is needed:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx --yes orionplace-react init my-marketplace
|
|
22
|
+
cd my-marketplace
|
|
23
|
+
npm install
|
|
24
|
+
npx orionplace-react set backend your-project.orionplace.app
|
|
25
|
+
npx orionplace-react add frontend shop.example.com
|
|
26
|
+
npm run dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The first command creates the Next.js configuration, App Router starter and
|
|
30
|
+
integration README. The first frontend becomes the local MIMIC domain. Open the
|
|
31
|
+
local URL printed by Next.js (normally http://localhost:3000); the starter's
|
|
32
|
+
AppLoad state should show `Loaded`. If it shows `Failed`, check your domain
|
|
33
|
+
mapping and backend response. The backend command accepts either a hostname or
|
|
34
|
+
its full HTTPS `/api` URL. The frontend command accepts a public hostname or its
|
|
35
|
+
root HTTPS URL.
|
|
36
|
+
|
|
37
|
+
## Existing Next.js 16 project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install orionplace-react
|
|
41
|
+
npx orionplace-react init --dry-run
|
|
42
|
+
npx orionplace-react set backend your-project.orionplace.app
|
|
43
|
+
npx orionplace-react add frontend shop.example.com
|
|
44
|
+
npm install
|
|
45
|
+
npm run dev
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The CLI preserves unrelated Next.js settings, scripts, and environment variables.
|
|
49
|
+
It supports the App Router in `app/` or `src/app/`, and never replaces your own
|
|
50
|
+
`layout.tsx`, `page.tsx`, global styles or `proxy.ts`. Resolve an existing `/api`
|
|
51
|
+
rewrite, redirect or catch-all route before setup. Projects using the Pages
|
|
52
|
+
Router or `output: 'export'` require manual integration.
|
|
53
|
+
|
|
54
|
+
No install or postinstall hook changes your application. Only explicitly running
|
|
55
|
+
the CLI writes files. Use `--cwd <directory>` to select a project and `--dry-run`
|
|
56
|
+
to inspect planned changes without writing them.
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx orionplace-react init [directory]
|
|
62
|
+
npx orionplace-react update
|
|
63
|
+
npx orionplace-react set backend your-project.orionplace.app
|
|
64
|
+
npx orionplace-react add frontend shop.example.com
|
|
65
|
+
npx orionplace-react add frontend www.shop.example.com
|
|
66
|
+
npx orionplace-react remove frontend www.shop.example.com
|
|
67
|
+
npx orionplace-react set mimic shop.example.com
|
|
68
|
+
npx orionplace-react help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Either domain command can run first. Both settings are needed for a usable mapping.
|
|
72
|
+
After setup, these npm script aliases are also available:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run orionplace:backend -- your-project.orionplace.app
|
|
76
|
+
npm run orionplace:add-frontend -- shop.example.com
|
|
77
|
+
npm run orionplace:remove-frontend -- www.shop.example.com
|
|
78
|
+
npm run orionplace:mimic -- shop.example.com
|
|
79
|
+
npm run orionplace:help
|
|
80
|
+
npm run orionplace:update
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`npm set` belongs to npm's own configuration system; use the commands above.
|
|
84
|
+
These commands configure local integration files. They do not provision a backend,
|
|
85
|
+
change your account, create DNS records, configure TLS, or deploy a VPS.
|
|
86
|
+
|
|
87
|
+
## Generated files
|
|
88
|
+
|
|
89
|
+
| Path | Purpose |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `app/api/[...orionplace]/route.ts` | Route handler serving `/api/...` from your project. |
|
|
92
|
+
| `app/layout.tsx` | Awaits initialization and provides the marketplace store. |
|
|
93
|
+
| `app/page.tsx` | Starter page showing the AppLoad state; replace it with your storefront. |
|
|
94
|
+
| `proxy.ts` | Marks personalized pages `private, no-store`. |
|
|
95
|
+
| `lib/orionplace/` | Server transport, browser transport, envelope checks and domain resolution. |
|
|
96
|
+
| `stores/orionplace.ts` | Typed snapshot, status, merged discovery and selectors. |
|
|
97
|
+
| `providers/OrionplaceProvider.tsx` | One store per browser session, hydrated from the server render. |
|
|
98
|
+
| `hooks/useOrionplaceAppLoad.ts` | Explicit initialize, refresh and reset actions. |
|
|
99
|
+
| `interfaces/orionplace/` | The AppLoad response envelope and every nested model. |
|
|
100
|
+
| `orionplace/runtime/marketplace.mjs` | Commented domain lookup and HTTP transport. |
|
|
101
|
+
| `orionplace/README.md` | Your settings, API examples, file explanations, verification and VPS deployment. |
|
|
102
|
+
| `orionplace/settings.json` | Domain values selected by the CLI. |
|
|
103
|
+
| `orionplace/.generated.json` | File hashes used to preserve your edits on subsequent setup. |
|
|
104
|
+
| `sitemaps/shop.example.com.cfg` | One line: `https://your-project.orionplace.app/api`. |
|
|
105
|
+
| `.env` | `MIMIC_FRONT_DOMAIN="shop.example.com"` for localhost. |
|
|
106
|
+
|
|
107
|
+
Projects that use `src/` receive the application source under `src/` instead.
|
|
108
|
+
All generated source is yours to read and customize. Later setup runs preserve
|
|
109
|
+
edited files and report which ones were preserved. Unchanged generated files can
|
|
110
|
+
be refreshed with `npx orionplace-react update`. A manually edited domain file is
|
|
111
|
+
never silently overwritten. Keep the hash manifest in version control.
|
|
112
|
+
|
|
113
|
+
## Multiple frontend domains
|
|
114
|
+
|
|
115
|
+
Each frontend gets its own `sitemaps/<domain>.cfg`. `add frontend` preserves other
|
|
116
|
+
domains and can be repeated safely. `set backend` updates every CLI-managed mapping
|
|
117
|
+
to the new personal API; it refuses to overwrite a manually edited mapping.
|
|
118
|
+
|
|
119
|
+
A new project starts with `MIMIC_FRONT_DOMAIN=""` in `.env`. Setting a backend
|
|
120
|
+
alone leaves it empty. The first added frontend becomes MIMIC; further additions
|
|
121
|
+
preserve that choice. `set mimic <domain>` selects an already-added frontend and
|
|
122
|
+
updates MIMIC without changing any domain mappings. Unknown domains are rejected;
|
|
123
|
+
add them with `add frontend` first.
|
|
124
|
+
|
|
125
|
+
`help`, `--help`, and `-h` display commands without writing files. The legacy
|
|
126
|
+
`set frontend <domain>` shortcut remains supported: it adds and selects a domain.
|
|
127
|
+
|
|
128
|
+
`remove frontend <domain>` removes that domain from the list and deletes only its
|
|
129
|
+
unchanged generated `.cfg`. Removing the MIMIC domain selects the first remaining
|
|
130
|
+
domain. Removing the last one clears MIMIC. Unknown or edited mappings cause a
|
|
131
|
+
clear error without changing files. Unrelated configurations remain untouched.
|
|
132
|
+
|
|
133
|
+
Restart development after domain changes so Next.js reloads `.env`. Production
|
|
134
|
+
reloads mapping files on each API request; reload the process environment when
|
|
135
|
+
changing MIMIC. Configure DNS, TLS, and the hosting proxy for each frontend
|
|
136
|
+
separately.
|
|
137
|
+
|
|
138
|
+
## Runtime behavior
|
|
139
|
+
|
|
140
|
+
The hosting proxy must overwrite `X-Front-Domain`, `X-Forwarded-Host`, and `Host`.
|
|
141
|
+
The gateway selects the first valid public domain in that order, with the request
|
|
142
|
+
URL hostname as another fallback. On localhost it uses `MIMIC_FRONT_DOMAIN`.
|
|
143
|
+
An unknown public domain returns `503`; it never uses another domain's mapping.
|
|
144
|
+
|
|
145
|
+
The corresponding `.cfg` is read on every request. Backend URLs must use HTTPS,
|
|
146
|
+
end in `/api`, and belong to `*.orionplace.app`. API methods, query strings, bodies,
|
|
147
|
+
authorization and cookies are forwarded. Backend statuses are retained and cookie
|
|
148
|
+
Domain attributes are removed to bind cookies to the storefront. Gateway responses
|
|
149
|
+
use `Cache-Control: no-store`. Network failures return `502`. Cross-site mutations
|
|
150
|
+
are rejected with `403`.
|
|
151
|
+
|
|
152
|
+
Browser requests use `/api/...`. Server components call the same gateway in process
|
|
153
|
+
through `lib/orionplace/appLoadServer.ts`, which forwards the current request's
|
|
154
|
+
domain, cookies and language.
|
|
155
|
+
|
|
156
|
+
Deploy `sitemaps` with the production server, or set an absolute `SITEMAPS_DIR`.
|
|
157
|
+
The production process reads `MIMIC_FRONT_DOMAIN` without rebuilding. Run the
|
|
158
|
+
server from the project root or explicitly point it at the configuration directory.
|
|
159
|
+
|
|
160
|
+
## AppLoad starter
|
|
161
|
+
|
|
162
|
+
The root layout awaits `POST /api/appload` in light mode before the page renders,
|
|
163
|
+
then hands the snapshot to the client provider. `getOrionplaceAppLoad()` is cached
|
|
164
|
+
per render, so a layout and a server component share one request. Client components
|
|
165
|
+
read the same snapshot through `useOrionplace(selector)`; hydration does not repeat
|
|
166
|
+
the request. `useOrionplaceAppLoad()` provides explicit `initialize`, `refresh` and
|
|
167
|
+
`reset` actions. Reading the store or hook never starts a request, and initialization
|
|
168
|
+
never opens an authentication prompt.
|
|
169
|
+
|
|
170
|
+
Live discovery from `page_data.unauth.paths` and, for a signed-in session,
|
|
171
|
+
`page_data.auth.paths`, is merged into one lookup without mutating the response.
|
|
172
|
+
Discovery describes availability; the backend still checks permissions on each call.
|
|
173
|
+
|
|
174
|
+
### Session and CSRF cookies
|
|
175
|
+
|
|
176
|
+
AppLoad establishes the guest/customer session and CSRF cookie through HTTP
|
|
177
|
+
`Set-Cookie` headers, not through `page_data.init`. The browser handles the signed
|
|
178
|
+
HttpOnly `sessionid` cookie; never parse, rewrite, log or store its value in the
|
|
179
|
+
store or localStorage. The readable `csrftoken` supplies `X-CSRFToken` for later
|
|
180
|
+
protected requests through the same-origin `/api/` gateway with credentials enabled.
|
|
181
|
+
Read the CSRF cookie again for every action because login can rotate it.
|
|
182
|
+
|
|
183
|
+
The server render forwards the incoming cookies to the gateway. React Server
|
|
184
|
+
Components cannot write response cookies, so a backend cookie created during that
|
|
185
|
+
render is not relayed to the browser; the browser establishes and updates its own
|
|
186
|
+
session through `/api/...` requests. `requestAppLoad()` returns the raw `Set-Cookie`
|
|
187
|
+
values if you relay them from your own route handler or server action.
|
|
188
|
+
|
|
189
|
+
Use `POST /api/csrf_renew` with `{}` to renew CSRF, then re-read the cookie.
|
|
190
|
+
The backend signals CSRF rejection with HTTP `419` and `page_data.csrf: false`;
|
|
191
|
+
a general `403` may mean permissions or origin rejection. Do not automatically
|
|
192
|
+
replay mutations. After confirmed login/logout, reset and refresh AppLoad.
|
|
193
|
+
Those store actions update data; they do not authenticate or delete cookies.
|
|
194
|
+
|
|
195
|
+
## Update an existing project
|
|
196
|
+
|
|
197
|
+
After the initial setup, use one command to get the latest package and components:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npx orionplace-react update
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
It runs the latest CLI, adds new template files, refreshes unchanged generated
|
|
204
|
+
files, updates the package dependency and runs `npm install` to synchronize the
|
|
205
|
+
lockfile and installed dependencies. Edited files are preserved and listed in the
|
|
206
|
+
output. Review their new versions under `node_modules/orionplace-react/templates`
|
|
207
|
+
or `runtime` and merge your changes manually when needed. Removed upstream files
|
|
208
|
+
are retained locally; the updater does not delete customer source.
|
|
209
|
+
|
|
210
|
+
Domain mappings, domain settings and `.env` remain unchanged. The command does
|
|
211
|
+
not deploy your application: restart development, or rebuild and redeploy the
|
|
212
|
+
production frontend after updating.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npx orionplace-react update --dry-run
|
|
216
|
+
npm run orionplace:update
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Dry-run downloads the latest CLI into npm's cache to inspect its templates but
|
|
220
|
+
leaves project files and dependencies unchanged. `update --local` applies the
|
|
221
|
+
version of the CLI being executed instead of fetching the latest CLI; it still
|
|
222
|
+
installs project dependencies. Use this only when intentionally selecting a version.
|
|
223
|
+
|
|
224
|
+
Updates require the existing `orionplace/settings.json` and `.generated.json`.
|
|
225
|
+
Conflicting untracked files stop the source update before any writes. If dependency
|
|
226
|
+
installation fails after source generation, run `npm install` and retry `update`.
|
|
227
|
+
|
|
228
|
+
## Documentation
|
|
229
|
+
|
|
230
|
+
- [Create a Next.js frontend](https://orionmarket.place/sandbox/new-project/react)
|
|
231
|
+
- [Marketplace API documentation](https://orionmarket.place/sandbox/)
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm install
|
|
237
|
+
npm test
|
|
238
|
+
npm pack --dry-run
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The published tarball includes only the CLI, runtime source, README, license,
|
|
242
|
+
starter templates, and package metadata.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { setupProject } from '../src/setup.mjs';
|
|
3
|
+
import { updateProject } from '../src/update.mjs';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
|
|
8
|
+
const help = `Orionplace React setup
|
|
9
|
+
|
|
10
|
+
From zero (Node.js 20.9+ with npm; run from the parent folder):
|
|
11
|
+
npx --yes orionplace-react init my-marketplace
|
|
12
|
+
cd my-marketplace
|
|
13
|
+
npm install
|
|
14
|
+
npx orionplace-react set backend your-project.orionplace.app
|
|
15
|
+
npx orionplace-react add frontend shop.example.com
|
|
16
|
+
npm run dev
|
|
17
|
+
|
|
18
|
+
Replace the backend with the domain from your orionmarket.place profile,
|
|
19
|
+
and the frontend with your storefront domain. Open Next.js's local URL;
|
|
20
|
+
the starter's AppLoad state should show Loaded.
|
|
21
|
+
|
|
22
|
+
Commands for an existing project (run from its root):
|
|
23
|
+
npx orionplace-react help
|
|
24
|
+
npx orionplace-react init [directory]
|
|
25
|
+
npx orionplace-react update
|
|
26
|
+
npx orionplace-react set backend <personal-backend.orionplace.app>
|
|
27
|
+
npx orionplace-react add frontend <shop.example.com>
|
|
28
|
+
npx orionplace-react remove frontend <shop.example.com>
|
|
29
|
+
npx orionplace-react set mimic <existing-frontend.example.com>
|
|
30
|
+
npx orionplace-react set frontend <shop.example.com>
|
|
31
|
+
|
|
32
|
+
Options: --cwd <directory> --dry-run --help --version
|
|
33
|
+
Update option: --local (apply this CLI version instead of fetching the latest release)
|
|
34
|
+
|
|
35
|
+
Update downloads the latest release, refreshes unedited source, and installs dependencies.
|
|
36
|
+
It preserves edited source, domain mappings and MIMIC. --dry-run leaves project files unchanged.
|
|
37
|
+
Set backend or add frontend also initializes the integration; either can come first.
|
|
38
|
+
MIMIC starts empty. The first added frontend sets it; later additions preserve it.
|
|
39
|
+
Set mimic selects an existing frontend without changing any domain mappings.
|
|
40
|
+
Set frontend is the legacy shortcut to add and select a frontend.
|
|
41
|
+
Set backend updates every managed frontend mapping. Remove frontend deletes only that mapping.
|
|
42
|
+
After creating a new project, run npm install and npm run dev in that directory.
|
|
43
|
+
Documentation and editable gateway source are generated in orionplace/.
|
|
44
|
+
Your App Router source is generated next to it, or under src/ when that layout is used.`;
|
|
45
|
+
try {
|
|
46
|
+
const argv = process.argv.slice(2);
|
|
47
|
+
if (!argv.length || argv[0] === 'help' || argv.includes('--help') || argv.includes('-h')) {
|
|
48
|
+
console.log(help);
|
|
49
|
+
} else if (argv.includes('--version')) {
|
|
50
|
+
console.log(JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version);
|
|
51
|
+
} else {
|
|
52
|
+
const positional = [];
|
|
53
|
+
let cwd = process.cwd();
|
|
54
|
+
let dryRun = false;
|
|
55
|
+
let local = false;
|
|
56
|
+
for (let index = 0; index < argv.length; index++) {
|
|
57
|
+
if (argv[index] === '--dry-run') dryRun = true;
|
|
58
|
+
else if (argv[index] === '--local') local = true;
|
|
59
|
+
else if (argv[index] === '--cwd') {
|
|
60
|
+
if (!argv[index + 1]) throw new Error('--cwd needs a directory');
|
|
61
|
+
cwd = resolve(argv[++index]);
|
|
62
|
+
} else if (argv[index].startsWith('--')) throw new Error(`Unknown option: ${argv[index]}`);
|
|
63
|
+
else positional.push(argv[index]);
|
|
64
|
+
}
|
|
65
|
+
const [command, target] = positional;
|
|
66
|
+
if (local && command !== 'update') throw new Error('--local is only supported by update.');
|
|
67
|
+
let result;
|
|
68
|
+
if (command === 'init' && positional.length <= 2) {
|
|
69
|
+
result = await setupProject({ cwd: target ? resolve(cwd, target) : cwd, dryRun });
|
|
70
|
+
} else if (command === 'update' && positional.length === 1) {
|
|
71
|
+
result = await updateProject({ cwd, dryRun, local });
|
|
72
|
+
} else if (((command === 'set' && ['backend', 'frontend', 'mimic'].includes(target)) ||
|
|
73
|
+
(['add', 'remove'].includes(command) && target === 'frontend')) && positional.length <= 3) {
|
|
74
|
+
let value = positional[2];
|
|
75
|
+
if (!value && process.stdin.isTTY) {
|
|
76
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
77
|
+
try { value = await prompt.question(`${target === 'backend' ? 'Personal backend API' : target === 'mimic' ? 'Existing frontend to use for MIMIC' : 'Public frontend domain'}: `); }
|
|
78
|
+
finally { prompt.close(); }
|
|
79
|
+
}
|
|
80
|
+
if (!value) throw new Error(`Provide the ${target} domain. See --help.`);
|
|
81
|
+
result = await setupProject({ cwd, action: command, field: target, value, dryRun });
|
|
82
|
+
} else throw new Error('Unknown command. See --help.');
|
|
83
|
+
if (result) {
|
|
84
|
+
console.log(`${dryRun ? 'Planned' : 'Updated'} ${result.root}`);
|
|
85
|
+
for (const path of result.changed) console.log(` ${result.removed.includes(path) ? 'Remove: ' : ''}${path}`);
|
|
86
|
+
for (const path of result.preserved) console.log(` Preserved your edits: ${path}`);
|
|
87
|
+
console.log(result.settings.backend && result.settings.frontend
|
|
88
|
+
? `Ready: ${result.settings.frontends.join(', ')} -> ${result.settings.backend}\nLocal MIMIC: ${result.settings.frontend}`
|
|
89
|
+
: `Next: ${result.settings.backend ? 'add frontend' : 'set backend'} to complete the domain mapping.`);
|
|
90
|
+
if (result.changed.some(path => path.endsWith('.cfg') || path === '.env')) console.log('Restart a running development server to reload MIMIC and domain mappings. Reload your production environment when changing MIMIC.');
|
|
91
|
+
if (result.isNewNext) console.log('Run npm install in the project, then npm run dev.');
|
|
92
|
+
console.log('Project documentation: orionplace/README.md');
|
|
93
|
+
if (command === 'update') console.log(dryRun
|
|
94
|
+
? 'Dependency installation would run after applying these changes. Project files were not changed.'
|
|
95
|
+
: 'Update complete. Restart development, or rebuild and redeploy your production frontend.');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`Orionplace: ${error.message}`);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orionplace-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Next.js integration and setup CLI for your Orionplace personal marketplace backend.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Orionplace",
|
|
8
|
+
"homepage": "https://orionmarket.place/sandbox/new-project/react",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"react",
|
|
11
|
+
"next",
|
|
12
|
+
"nextjs",
|
|
13
|
+
"orionplace",
|
|
14
|
+
"marketplace"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20.9.0"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"orionplace-react": "./bin/orionplace-react.mjs"
|
|
21
|
+
},
|
|
22
|
+
"main": "./runtime/marketplace.mjs",
|
|
23
|
+
"types": "./runtime/marketplace.d.mts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./runtime/marketplace.d.mts",
|
|
27
|
+
"import": "./runtime/marketplace.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./runtime": {
|
|
30
|
+
"types": "./runtime/marketplace.d.mts",
|
|
31
|
+
"import": "./runtime/marketplace.mjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin",
|
|
36
|
+
"src",
|
|
37
|
+
"runtime",
|
|
38
|
+
"templates",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test test/*.test.mjs"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"next": "^16.0.0"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"registry": "https://registry.npmjs.org/"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"esbuild": "^0.27.7",
|
|
54
|
+
"zustand": "^5.0.15"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface MarketplaceConfiguration {
|
|
2
|
+
mimicFrontDomain?: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface MarketplaceTenant {
|
|
6
|
+
domain: string;
|
|
7
|
+
api: URL;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export declare function normalizeFrontDomain(value: unknown): string | null;
|
|
11
|
+
|
|
12
|
+
export declare function resolveFrontDomain(request: Request, mimic?: string): string | null;
|
|
13
|
+
|
|
14
|
+
export declare function readMarketplace(
|
|
15
|
+
request: Request,
|
|
16
|
+
configuration?: MarketplaceConfiguration,
|
|
17
|
+
): Promise<MarketplaceTenant>;
|
|
18
|
+
|
|
19
|
+
export declare function forwardMarketplace(
|
|
20
|
+
request: Request,
|
|
21
|
+
configuration?: MarketplaceConfiguration,
|
|
22
|
+
): Promise<Response>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
4
|
+
|
|
5
|
+
const environment = process.env;
|
|
6
|
+
|
|
7
|
+
const hostnamePattern =
|
|
8
|
+
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
9
|
+
|
|
10
|
+
/** @param {unknown} value */
|
|
11
|
+
export function normalizeFrontDomain(value) {
|
|
12
|
+
if (typeof value !== 'string') return null;
|
|
13
|
+
const domain = value.split(',')[0].trim().toLowerCase().replace(/\.$/, '');
|
|
14
|
+
return hostnamePattern.test(domain) && !isIP(domain) ? domain : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @param {Request} request @param {string} mimic */
|
|
18
|
+
export function resolveFrontDomain(
|
|
19
|
+
request,
|
|
20
|
+
mimic = environment.MIMIC_FRONT_DOMAIN || '',
|
|
21
|
+
) {
|
|
22
|
+
// The hosting proxy must overwrite these headers. Public hosts win over MIMIC.
|
|
23
|
+
for (const candidate of [
|
|
24
|
+
request.headers.get('x-front-domain'),
|
|
25
|
+
request.headers.get('x-forwarded-host'),
|
|
26
|
+
request.headers.get('host'),
|
|
27
|
+
new URL(request.url).hostname,
|
|
28
|
+
]) {
|
|
29
|
+
const domain = normalizeFrontDomain(candidate);
|
|
30
|
+
if (domain) return domain;
|
|
31
|
+
}
|
|
32
|
+
return normalizeFrontDomain(mimic);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {Request} request @param {{mimicFrontDomain?: string}} configuration */
|
|
36
|
+
export async function readMarketplace(request, configuration = {}) {
|
|
37
|
+
const domain = resolveFrontDomain(request, configuration.mimicFrontDomain);
|
|
38
|
+
if (!domain) throw new Error('Invalid storefront domain');
|
|
39
|
+
|
|
40
|
+
const directory = environment.SITEMAPS_DIR || resolve('sitemaps');
|
|
41
|
+
const source = await readFile(resolve(directory, `${domain}.cfg`), 'utf8');
|
|
42
|
+
const lines = source
|
|
43
|
+
.split(/\r?\n/)
|
|
44
|
+
.map((line) => line.trim())
|
|
45
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
46
|
+
if (lines.length !== 1) throw new Error('Expected one API URL');
|
|
47
|
+
const value = lines[0];
|
|
48
|
+
const api = new URL(value.includes('://') ? value : `https://${value}`);
|
|
49
|
+
api.pathname = api.pathname.replace(/\/+$/, '');
|
|
50
|
+
if (
|
|
51
|
+
api.protocol !== 'https:' ||
|
|
52
|
+
!hostnamePattern.test(api.hostname) ||
|
|
53
|
+
!api.hostname.endsWith('.orionplace.app') ||
|
|
54
|
+
isIP(api.hostname) ||
|
|
55
|
+
api.port ||
|
|
56
|
+
api.username ||
|
|
57
|
+
api.password ||
|
|
58
|
+
api.search ||
|
|
59
|
+
api.hash ||
|
|
60
|
+
api.pathname !== '/api'
|
|
61
|
+
) {
|
|
62
|
+
throw new Error('Expected https://<project>.orionplace.app/api');
|
|
63
|
+
}
|
|
64
|
+
return { domain, api };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {number} status @param {string} message */
|
|
68
|
+
function failure(status, message) {
|
|
69
|
+
return Response.json(
|
|
70
|
+
{ error: message },
|
|
71
|
+
{
|
|
72
|
+
status,
|
|
73
|
+
headers: {
|
|
74
|
+
'Cache-Control': 'no-store',
|
|
75
|
+
'Vary': 'Host, X-Forwarded-Host, X-Front-Domain',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @param {Request} request @param {{mimicFrontDomain?: string}} configuration */
|
|
82
|
+
export async function forwardMarketplace(request, configuration = {}) {
|
|
83
|
+
let tenant;
|
|
84
|
+
try {
|
|
85
|
+
tenant = await readMarketplace(request, configuration);
|
|
86
|
+
} catch {
|
|
87
|
+
return failure(
|
|
88
|
+
503,
|
|
89
|
+
'Storefront domain configuration is missing or invalid',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const incoming = new URL(request.url);
|
|
94
|
+
if (!incoming.pathname.startsWith('/api/'))
|
|
95
|
+
return failure(404, 'Unknown API path');
|
|
96
|
+
// Prevent encoded separators or traversal from escaping the /api namespace.
|
|
97
|
+
let path;
|
|
98
|
+
try {
|
|
99
|
+
path = decodeURIComponent(incoming.pathname);
|
|
100
|
+
} catch {
|
|
101
|
+
return failure(400, 'Invalid API path');
|
|
102
|
+
}
|
|
103
|
+
if (
|
|
104
|
+
path.includes('\\') ||
|
|
105
|
+
/%(?:2e|2f|5c)/i.test(path) ||
|
|
106
|
+
path.split('/').some((part) => part === '.' || part === '..')
|
|
107
|
+
) {
|
|
108
|
+
return failure(400, 'Invalid API path');
|
|
109
|
+
}
|
|
110
|
+
const target = new URL(
|
|
111
|
+
incoming.pathname + incoming.search,
|
|
112
|
+
tenant.api.origin,
|
|
113
|
+
);
|
|
114
|
+
if (!target.pathname.startsWith('/api/'))
|
|
115
|
+
return failure(400, 'Invalid API path');
|
|
116
|
+
|
|
117
|
+
const origin = request.headers.get('origin');
|
|
118
|
+
const publicHost = request.headers.get('host') || incoming.host;
|
|
119
|
+
const originHosts = [publicHost, tenant.domain];
|
|
120
|
+
if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
|
|
121
|
+
try {
|
|
122
|
+
if (
|
|
123
|
+
request.headers.get('sec-fetch-site') === 'cross-site' ||
|
|
124
|
+
(origin && !originHosts.includes(new URL(origin).host))
|
|
125
|
+
) {
|
|
126
|
+
return failure(403, 'Cross-site API request rejected');
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
return failure(403, 'Invalid Origin');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// An allowlist excludes hop-by-hop and client-supplied routing headers.
|
|
134
|
+
const headers = new Headers();
|
|
135
|
+
for (const name of [
|
|
136
|
+
'accept',
|
|
137
|
+
'content-type',
|
|
138
|
+
'authorization',
|
|
139
|
+
'cookie',
|
|
140
|
+
'origin',
|
|
141
|
+
'referer',
|
|
142
|
+
'x-csrftoken',
|
|
143
|
+
'accept-language',
|
|
144
|
+
'user-agent',
|
|
145
|
+
'x-tz',
|
|
146
|
+
]) {
|
|
147
|
+
const value = request.headers.get(name);
|
|
148
|
+
if (value) headers.set(name, value);
|
|
149
|
+
}
|
|
150
|
+
headers.set('X-Front-Domain', tenant.domain);
|
|
151
|
+
headers.set('Accept-Encoding', 'identity');
|
|
152
|
+
const clientIp =
|
|
153
|
+
request.headers.get('x-init-ip') ||
|
|
154
|
+
request.headers.get('x-real-ip') ||
|
|
155
|
+
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
|
|
156
|
+
if (clientIp) headers.set('X-Init-Ip', clientIp);
|
|
157
|
+
/** @type {RequestInit & { duplex: 'half' }} */
|
|
158
|
+
const options = {
|
|
159
|
+
method: request.method,
|
|
160
|
+
headers,
|
|
161
|
+
redirect: 'manual',
|
|
162
|
+
cache: 'no-store',
|
|
163
|
+
signal: AbortSignal.any([request.signal, AbortSignal.timeout(30_000)]),
|
|
164
|
+
duplex: 'half',
|
|
165
|
+
};
|
|
166
|
+
if (!['GET', 'HEAD'].includes(request.method)) options.body = request.body;
|
|
167
|
+
|
|
168
|
+
let upstream;
|
|
169
|
+
try {
|
|
170
|
+
upstream = await fetch(target, options);
|
|
171
|
+
} catch {
|
|
172
|
+
return failure(502, 'Marketplace backend is unavailable');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const responseHeaders = new Headers();
|
|
176
|
+
for (const name of [
|
|
177
|
+
'content-type',
|
|
178
|
+
'content-disposition',
|
|
179
|
+
'www-authenticate',
|
|
180
|
+
'retry-after',
|
|
181
|
+
'allow',
|
|
182
|
+
]) {
|
|
183
|
+
const value = upstream.headers.get(name);
|
|
184
|
+
if (value) responseHeaders.set(name, value);
|
|
185
|
+
}
|
|
186
|
+
// Keep each cookie separate; remove the backend Domain attribute only.
|
|
187
|
+
for (const cookie of upstream.headers.getSetCookie()) {
|
|
188
|
+
responseHeaders.append(
|
|
189
|
+
'Set-Cookie',
|
|
190
|
+
cookie.replace(/;\s*Domain=[^;]*/gi, ''),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const location = upstream.headers.get('location');
|
|
194
|
+
if (location) {
|
|
195
|
+
const destination = new URL(location, target);
|
|
196
|
+
responseHeaders.set(
|
|
197
|
+
'Location',
|
|
198
|
+
destination.origin === tenant.api.origin
|
|
199
|
+
? destination.pathname + destination.search + destination.hash
|
|
200
|
+
: destination.href,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
responseHeaders.set('Cache-Control', 'no-store');
|
|
204
|
+
responseHeaders.set('Vary', 'Host, X-Forwarded-Host, X-Front-Domain');
|
|
205
|
+
// fetch decodes compressed bodies: do not reuse upstream Content-Length/Encoding.
|
|
206
|
+
return new Response(upstream.body, {
|
|
207
|
+
status: upstream.status,
|
|
208
|
+
statusText: upstream.statusText,
|
|
209
|
+
headers: responseHeaders,
|
|
210
|
+
});
|
|
211
|
+
}
|