@gnldev/auth 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 +201 -0
- package/README.md +138 -0
- package/dist/adapter.d.ts +38 -0
- package/dist/adapter.js +79 -0
- package/dist/adapter.js.map +1 -0
- package/dist/gate.d.ts +32 -0
- package/dist/gate.js +122 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/role-auth.d.ts +34 -0
- package/dist/role-auth.js +199 -0
- package/dist/role-auth.js.map +1 -0
- package/dist/safe-equal.d.ts +2 -0
- package/dist/safe-equal.js +15 -0
- package/dist/safe-equal.js.map +1 -0
- package/dist/same-site.d.ts +15 -0
- package/dist/same-site.js +57 -0
- package/dist/same-site.js.map +1 -0
- package/dist/scope.d.ts +53 -0
- package/dist/scope.js +50 -0
- package/dist/scope.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Karaca Yılmaz (https://gnl.dev)
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @gnldev/auth
|
|
2
|
+
|
|
3
|
+
The auth contract the rest of the framework speaks, plus a role-based default you can use as-is.
|
|
4
|
+
|
|
5
|
+
Auth is **opt-in**: leave it out and the REST API and Studio stay open (fine for local work). Wire a
|
|
6
|
+
provider in and every route is gated. In production, a missing provider is an error rather than a
|
|
7
|
+
silent open door.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
> Install: `pnpm add @gnldev/auth` — or use it from a [repo clone](https://github.com/Karaca7/gnldev): `pnpm install && pnpm -r build`.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @gnldev/auth
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Four credential classes
|
|
18
|
+
|
|
19
|
+
Pick the class by **who holds the token**, not by how much it needs to do. Credentials can be a bearer
|
|
20
|
+
token or a user/password pair for basic auth — comparison is constant-time.
|
|
21
|
+
|
|
22
|
+
| Class | Who holds it | Organization | Can |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `superAdmin` | you, running the platform | none, by design | everything, across every organization |
|
|
25
|
+
| `admin` | a customer's operator | bound | manage that organization: Studio, budgets, users, workflows |
|
|
26
|
+
| `client` | a customer's **backend server** | bound | run agents, cancel runs, read — nothing else |
|
|
27
|
+
| `viewer` | a read-only operator | bound | read |
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { roleAuth } from '@gnldev/auth';
|
|
31
|
+
import { createRestApi } from '@gnldev/server';
|
|
32
|
+
|
|
33
|
+
const auth = roleAuth({
|
|
34
|
+
// The credential your APPLICATION carries. It serves many end users under one token, so every
|
|
35
|
+
// request names the end user it acts for (see below).
|
|
36
|
+
client: { token: process.env.GNL_CLIENT_TOKEN, orgId: 'acme' },
|
|
37
|
+
// The credential a PERSON carries, for Studio.
|
|
38
|
+
admin: { token: process.env.GNL_ADMIN_TOKEN, orgId: 'acme' },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const app = createRestApi(config, { auth });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`roleAuth` returns `undefined` when no class is configured — that is what keeps auth opt-in.
|
|
45
|
+
|
|
46
|
+
### Give your application `client`, not `admin`
|
|
47
|
+
|
|
48
|
+
`admin` is an operator credential: it cancels runs, edits budgets, manages users and reads the whole
|
|
49
|
+
organization's history. An application that only needs to run an agent has no business holding that,
|
|
50
|
+
and if it leaks, none of those are things you wanted a leaked key to reach.
|
|
51
|
+
|
|
52
|
+
`client` is a **whitelist** — `agents:run`, `workflow:run`, `run:cancel`, plus reads. Anything else is
|
|
53
|
+
refused, including routes added in later versions. That is the direction you want the default to fail.
|
|
54
|
+
|
|
55
|
+
### A `client` names the end user it acts for
|
|
56
|
+
|
|
57
|
+
One application credential serves many end users, so the request says which one:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const GNL = process.env.GNL_URL!;
|
|
61
|
+
const CLIENT_TOKEN = process.env.GNL_CLIENT_TOKEN!;
|
|
62
|
+
|
|
63
|
+
await fetch(`${GNL}/agents/support/run`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { authorization: `Bearer ${CLIENT_TOKEN}`, 'content-type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
runId: 'r-882',
|
|
68
|
+
prompt: 'where is my order',
|
|
69
|
+
threadId: 't-ayse-1',
|
|
70
|
+
resourceId: 'u-ayse', // ← WHOSE request this is
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`resourceId` is what separates one end user's conversation, memory and runs from another's. A `client`
|
|
76
|
+
request that omits it is refused (`400`) rather than served unscoped — without a subject there is
|
|
77
|
+
nothing to keep two of your users apart. Operator classes may omit it: they work across the
|
|
78
|
+
organization by design.
|
|
79
|
+
|
|
80
|
+
You can then ask for one user's data, and have ownership checked:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
GET /runs?resourceId=u-ayse → only that user's runs
|
|
84
|
+
GET /threads?resourceId=u-ayse → only that user's conversations
|
|
85
|
+
GET /runs/r-882?resourceId=u-mehmet → 403, the run belongs to someone else
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> **Keep the client token on your server.** It is trusted to say who it acts for, so anyone holding it
|
|
89
|
+
> can claim any `resourceId`. That is safe in your backend, where you already know who your user is.
|
|
90
|
+
> It is not safe in a browser or a mobile app — ship neither the token nor a proxy that forwards a
|
|
91
|
+
> caller-supplied `resourceId` unchecked.
|
|
92
|
+
|
|
93
|
+
### `superAdmin` is stated, never inferred
|
|
94
|
+
|
|
95
|
+
An identity belonging to no organization is **not** treated as an operator by accident. Once `org` is
|
|
96
|
+
configured, an unbound identity is refused unless it is declared `superAdmin`; otherwise a forgotten
|
|
97
|
+
`orgId` would quietly mint a cross-organization super-admin.
|
|
98
|
+
|
|
99
|
+
## The contract
|
|
100
|
+
|
|
101
|
+
| Export | What it is |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `AuthProvider` | `authenticate(request)` → a `Principal` or `null`, then `authorize(principal, request, ctx)` → `{ allow }` |
|
|
104
|
+
| `roleAuth` | The bundled provider above |
|
|
105
|
+
| `CLIENT_WRITES` | The exact set of writes a `client` may perform — read it rather than guessing |
|
|
106
|
+
| `PLATFORM_ADMIN_ROLE` / `isPlatformAdmin` | The reserved cross-organization grant `superAdmin` carries |
|
|
107
|
+
| `makeGate` | Turns a provider into a gate a host can apply to routes |
|
|
108
|
+
| `principalOf` | Reads the principal a gate resolved for a request |
|
|
109
|
+
| `normalizeAuth` / `fromReadWrite` | Accepts the older `{ read, write }` predicate pair and adapts it to the provider interface |
|
|
110
|
+
|
|
111
|
+
## Extending it
|
|
112
|
+
|
|
113
|
+
`AuthProvider` is the seam. Anything implementing it works: your own JWT logic, an identity service,
|
|
114
|
+
or the paid `@gnldev/auth-ee`, which plugs into this same interface to add SSO, RBAC, fine-grained
|
|
115
|
+
authorization and a relational user store. Nothing in this package needs to change to swap providers.
|
|
116
|
+
|
|
117
|
+
A provider may also declare `capabilities()` — that is how a host learns whether features like
|
|
118
|
+
role-based access or multi-organization scoping are available.
|
|
119
|
+
|
|
120
|
+
### What the paid provider adds on top of these four
|
|
121
|
+
|
|
122
|
+
The four classes above are **fixed**: `viewer` reads everything inside its organization, and there is
|
|
123
|
+
no way to narrow it. `@gnldev/auth-ee` replaces the classes with per-user permissions, so you choose
|
|
124
|
+
what each person sees:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
Ayşe runs:read ✓ threads:read ✗ money:read ✗ → sees that a run failed,
|
|
128
|
+
not what the customer typed into it
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Reads are named the same way writes are — `runs`, `threads`, `money`, `audit`, `users`, `catalog` —
|
|
132
|
+
and `*:read` still means all of them, so nothing an existing grant could reach becomes unreachable.
|
|
133
|
+
Isolation itself is **not** the paid part: the organization boundary, the `client` whitelist and the
|
|
134
|
+
`resourceId` rules above are all in this package, and none of them turn on when you pay.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
Apache-2.0 — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AuthProvider } from './types.js';
|
|
2
|
+
/** Old role-based hook: read = GET (viewer), write = POST/PUT/PATCH/DELETE (admin). */
|
|
3
|
+
export interface ReadWriteAuth {
|
|
4
|
+
read?: (req: Request) => boolean | Promise<boolean>;
|
|
5
|
+
write?: (req: Request) => boolean | Promise<boolean>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Can this provider ever produce a principal?
|
|
9
|
+
*
|
|
10
|
+
* `authenticate() === null` is ambiguous: it means EITHER "this request carried no token" (a
|
|
11
|
+
* roleAuth caller who should get 401) OR "this provider has no principal model at all and never
|
|
12
|
+
* will" (the {read,write} pair below). A host that scopes organizations by identity has to tell
|
|
13
|
+
* those apart — the first is a per-request condition, the second is a configuration that cannot
|
|
14
|
+
* isolate anything, because there is no identity to bind an org to. Collapsing them makes the
|
|
15
|
+
* unauthenticated caller's 401 turn into a misleading 403, or leaves the unsafe configuration open.
|
|
16
|
+
*
|
|
17
|
+
* Absent on hand-written providers, so `!== false` is the compatible reading: unknown means capable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function bindsIdentity(auth: AuthProvider | undefined): boolean;
|
|
20
|
+
/** {read,write} → AuthProvider. If there's no fn in that direction, it's unrestricted (preserves the existing `!fn || fn(c)` behavior). */
|
|
21
|
+
export declare function fromReadWrite(rw: ReadWriteAuth): AuthProvider;
|
|
22
|
+
/**
|
|
23
|
+
* AuthProvider | {read,write} | undefined → AuthProvider | undefined (hosts reduce to a single type).
|
|
24
|
+
*
|
|
25
|
+
* Throws on an object that is neither, because the alternative is the worst possible answer. The
|
|
26
|
+
* previous reading was "not a provider ⇒ it must be {read,write}", and `fromReadWrite` treats a
|
|
27
|
+
* missing direction as unrestricted — so ANY unrecognised object became a provider that allows
|
|
28
|
+
* everything. The realistic way to hit that is the credential map the scaffold writes:
|
|
29
|
+
*
|
|
30
|
+
* createRestApi({ auth: { admin: { token: 's3cret' } } }) // looks protected, allows everyone
|
|
31
|
+
*
|
|
32
|
+
* That exact shape shipped once already: @gnldev/studio grew `resolveConfigAuth` for it, but
|
|
33
|
+
* @gnldev/server still calls this directly, so the hole stayed open on the other host. Refusing here
|
|
34
|
+
* closes it for every caller and turns a silent, invisible failure into an error at startup — the one
|
|
35
|
+
* moment it can still be fixed. A caller that really wants no restrictions passes `undefined`; that
|
|
36
|
+
* intent is expressible and needs no guessing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function normalizeAuth(auth?: AuthProvider | ReadWriteAuth): AuthProvider | undefined;
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Can this provider ever produce a principal?
|
|
3
|
+
*
|
|
4
|
+
* `authenticate() === null` is ambiguous: it means EITHER "this request carried no token" (a
|
|
5
|
+
* roleAuth caller who should get 401) OR "this provider has no principal model at all and never
|
|
6
|
+
* will" (the {read,write} pair below). A host that scopes organizations by identity has to tell
|
|
7
|
+
* those apart — the first is a per-request condition, the second is a configuration that cannot
|
|
8
|
+
* isolate anything, because there is no identity to bind an org to. Collapsing them makes the
|
|
9
|
+
* unauthenticated caller's 401 turn into a misleading 403, or leaves the unsafe configuration open.
|
|
10
|
+
*
|
|
11
|
+
* Absent on hand-written providers, so `!== false` is the compatible reading: unknown means capable.
|
|
12
|
+
*/
|
|
13
|
+
export function bindsIdentity(auth) {
|
|
14
|
+
return !!auth && auth.bindsIdentity !== false;
|
|
15
|
+
}
|
|
16
|
+
/** {read,write} → AuthProvider. If there's no fn in that direction, it's unrestricted (preserves the existing `!fn || fn(c)` behavior). */
|
|
17
|
+
export function fromReadWrite(rw) {
|
|
18
|
+
return {
|
|
19
|
+
bindsIdentity: false,
|
|
20
|
+
authenticate() {
|
|
21
|
+
return null; // no principal model; the decision is made in the predicate.
|
|
22
|
+
},
|
|
23
|
+
async authorize(_p, req, ctx) {
|
|
24
|
+
const fn = ctx.action === 'read' ? rw.read : rw.write;
|
|
25
|
+
if (!fn)
|
|
26
|
+
return { allow: true };
|
|
27
|
+
const ok = await fn(req);
|
|
28
|
+
return ok ? { allow: true } : { allow: false, status: ctx.action === 'write' ? 403 : 401 };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function isProvider(x) {
|
|
33
|
+
return typeof x.authorize === 'function';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* AuthProvider | {read,write} | undefined → AuthProvider | undefined (hosts reduce to a single type).
|
|
37
|
+
*
|
|
38
|
+
* Throws on an object that is neither, because the alternative is the worst possible answer. The
|
|
39
|
+
* previous reading was "not a provider ⇒ it must be {read,write}", and `fromReadWrite` treats a
|
|
40
|
+
* missing direction as unrestricted — so ANY unrecognised object became a provider that allows
|
|
41
|
+
* everything. The realistic way to hit that is the credential map the scaffold writes:
|
|
42
|
+
*
|
|
43
|
+
* createRestApi({ auth: { admin: { token: 's3cret' } } }) // looks protected, allows everyone
|
|
44
|
+
*
|
|
45
|
+
* That exact shape shipped once already: @gnldev/studio grew `resolveConfigAuth` for it, but
|
|
46
|
+
* @gnldev/server still calls this directly, so the hole stayed open on the other host. Refusing here
|
|
47
|
+
* closes it for every caller and turns a silent, invisible failure into an error at startup — the one
|
|
48
|
+
* moment it can still be fixed. A caller that really wants no restrictions passes `undefined`; that
|
|
49
|
+
* intent is expressible and needs no guessing.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeAuth(auth) {
|
|
52
|
+
if (!auth)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (isProvider(auth))
|
|
55
|
+
return auth;
|
|
56
|
+
const rw = auth;
|
|
57
|
+
// Each side that is PRESENT must be a function. The check used to be `read is fn OR write is fn`,
|
|
58
|
+
// which short-circuits: `{ read: fn, write: { admin: { token } } }` — one direction a predicate, the
|
|
59
|
+
// other a credential map, which is what a half-finished config looks like — passed here and then
|
|
60
|
+
// threw at REQUEST time, on the first write, in production. That is the silent invisible failure
|
|
61
|
+
// this whole function exists to move to startup.
|
|
62
|
+
const sides = ['read', 'write'].filter((k) => rw[k] !== undefined);
|
|
63
|
+
if (sides.length > 0) {
|
|
64
|
+
const bad = sides.filter((k) => typeof rw[k] !== 'function');
|
|
65
|
+
if (bad.length === 0)
|
|
66
|
+
return fromReadWrite(rw);
|
|
67
|
+
throw new TypeError(`@gnldev/auth: \`auth.${bad.join('\` and \`auth.')}\` must be a function ` +
|
|
68
|
+
`(got ${bad.map((k) => `${k}: ${typeof rw[k]}`).join(', ')}). ` +
|
|
69
|
+
`A {read, write} pair takes predicates; a credential map like { admin: { token } } has to be ` +
|
|
70
|
+
`wrapped: \`roleAuth({ admin: { token } })\`. Left as it is, this authorises nothing until the ` +
|
|
71
|
+
`first request and then throws there instead.`);
|
|
72
|
+
}
|
|
73
|
+
const keys = Object.keys(auth).slice(0, 5).join(', ') || '(no keys)';
|
|
74
|
+
throw new TypeError(`@gnldev/auth: \`auth\` is neither an AuthProvider (no \`authorize\` function) nor a {read, write} ` +
|
|
75
|
+
`pair (no \`read\`/\`write\` function). Got an object with: ${keys}. ` +
|
|
76
|
+
`If this is a credential map like { admin: { token } }, wrap it: \`roleAuth({ admin: { token } })\`. ` +
|
|
77
|
+
`Passing it directly would authorise every request. For no restrictions, omit \`auth\` entirely.`);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,IAA8B;IAC1D,OAAO,CAAC,CAAC,IAAI,IAAK,IAAoC,CAAC,aAAa,KAAK,KAAK,CAAC;AACjF,CAAC;AAED,2IAA2I;AAC3I,MAAM,UAAU,aAAa,CAAC,EAAiB;IAC7C,OAAO;QACL,aAAa,EAAE,KAAK;QACpB,YAAY;YACV,OAAO,IAAI,CAAC,CAAC,6DAA6D;QAC5E,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,GAAY,EAAE,GAAgB;YAChD,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC;YACtD,IAAI,CAAC,EAAE;gBAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;YACzB,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QAC7F,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,CAA+B;IACjD,OAAO,OAAQ,CAAkB,CAAC,SAAS,KAAK,UAAU,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,aAAa,CAAC,IAAmC;IAC/D,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,IAAI,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,MAAM,EAAE,GAAG,IAAqB,CAAC;IACjC,kGAAkG;IAClG,qGAAqG;IACrG,iGAAiG;IACjG,iGAAiG;IACjG,iDAAiD;IACjD,MAAM,KAAK,GAAI,CAAC,MAAM,EAAE,OAAO,CAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;IAC9E,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC;QAC7D,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC;QAC/C,MAAM,IAAI,SAAS,CACjB,wBAAwB,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,wBAAwB;YAC1E,QAAQ,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YAC/D,8FAA8F;YAC9F,gGAAgG;YAChG,8CAA8C,CAC/C,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAc,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC;IAC/E,MAAM,IAAI,SAAS,CACjB,oGAAoG;QACpG,8DAA8D,IAAI,IAAI;QACtE,sGAAsG;QACtG,iGAAiG,CAClG,CAAC;AACJ,CAAC","sourcesContent":["// Backward compatibility: wraps the old {read,write} predicate pair (StudioAuth / examples/app) into an AuthProvider.\nimport type { AuthProvider, Decision, AuthContext } from './types.js';\n\n/** Old role-based hook: read = GET (viewer), write = POST/PUT/PATCH/DELETE (admin). */\nexport interface ReadWriteAuth {\n read?: (req: Request) => boolean | Promise<boolean>;\n write?: (req: Request) => boolean | Promise<boolean>;\n}\n\n/**\n * Can this provider ever produce a principal?\n *\n * `authenticate() === null` is ambiguous: it means EITHER \"this request carried no token\" (a\n * roleAuth caller who should get 401) OR \"this provider has no principal model at all and never\n * will\" (the {read,write} pair below). A host that scopes organizations by identity has to tell\n * those apart — the first is a per-request condition, the second is a configuration that cannot\n * isolate anything, because there is no identity to bind an org to. Collapsing them makes the\n * unauthenticated caller's 401 turn into a misleading 403, or leaves the unsafe configuration open.\n *\n * Absent on hand-written providers, so `!== false` is the compatible reading: unknown means capable.\n */\nexport function bindsIdentity(auth: AuthProvider | undefined): boolean {\n return !!auth && (auth as { bindsIdentity?: boolean }).bindsIdentity !== false;\n}\n\n/** {read,write} → AuthProvider. If there's no fn in that direction, it's unrestricted (preserves the existing `!fn || fn(c)` behavior). */\nexport function fromReadWrite(rw: ReadWriteAuth): AuthProvider {\n return {\n bindsIdentity: false,\n authenticate() {\n return null; // no principal model; the decision is made in the predicate.\n },\n async authorize(_p, req: Request, ctx: AuthContext): Promise<Decision> {\n const fn = ctx.action === 'read' ? rw.read : rw.write;\n if (!fn) return { allow: true };\n const ok = await fn(req);\n return ok ? { allow: true } : { allow: false, status: ctx.action === 'write' ? 403 : 401 };\n },\n };\n}\n\nfunction isProvider(x: AuthProvider | ReadWriteAuth): x is AuthProvider {\n return typeof (x as AuthProvider).authorize === 'function';\n}\n\n/**\n * AuthProvider | {read,write} | undefined → AuthProvider | undefined (hosts reduce to a single type).\n *\n * Throws on an object that is neither, because the alternative is the worst possible answer. The\n * previous reading was \"not a provider ⇒ it must be {read,write}\", and `fromReadWrite` treats a\n * missing direction as unrestricted — so ANY unrecognised object became a provider that allows\n * everything. The realistic way to hit that is the credential map the scaffold writes:\n *\n * createRestApi({ auth: { admin: { token: 's3cret' } } }) // looks protected, allows everyone\n *\n * That exact shape shipped once already: @gnldev/studio grew `resolveConfigAuth` for it, but\n * @gnldev/server still calls this directly, so the hole stayed open on the other host. Refusing here\n * closes it for every caller and turns a silent, invisible failure into an error at startup — the one\n * moment it can still be fixed. A caller that really wants no restrictions passes `undefined`; that\n * intent is expressible and needs no guessing.\n */\nexport function normalizeAuth(auth?: AuthProvider | ReadWriteAuth): AuthProvider | undefined {\n if (!auth) return undefined;\n if (isProvider(auth)) return auth;\n const rw = auth as ReadWriteAuth;\n // Each side that is PRESENT must be a function. The check used to be `read is fn OR write is fn`,\n // which short-circuits: `{ read: fn, write: { admin: { token } } }` — one direction a predicate, the\n // other a credential map, which is what a half-finished config looks like — passed here and then\n // threw at REQUEST time, on the first write, in production. That is the silent invisible failure\n // this whole function exists to move to startup.\n const sides = (['read', 'write'] as const).filter((k) => rw[k] !== undefined);\n if (sides.length > 0) {\n const bad = sides.filter((k) => typeof rw[k] !== 'function');\n if (bad.length === 0) return fromReadWrite(rw);\n throw new TypeError(\n `@gnldev/auth: \\`auth.${bad.join('\\` and \\`auth.')}\\` must be a function ` +\n `(got ${bad.map((k) => `${k}: ${typeof rw[k]}`).join(', ')}). ` +\n `A {read, write} pair takes predicates; a credential map like { admin: { token } } has to be ` +\n `wrapped: \\`roleAuth({ admin: { token } })\\`. Left as it is, this authorises nothing until the ` +\n `first request and then throws there instead.`,\n );\n }\n const keys = Object.keys(auth as object).slice(0, 5).join(', ') || '(no keys)';\n throw new TypeError(\n `@gnldev/auth: \\`auth\\` is neither an AuthProvider (no \\`authorize\\` function) nor a {read, write} ` +\n `pair (no \\`read\\`/\\`write\\` function). Got an object with: ${keys}. ` +\n `If this is a credential map like { admin: { token } }, wrap it: \\`roleAuth({ admin: { token } })\\`. ` +\n `Passing it directly would authorise every request. For no restrictions, omit \\`auth\\` entirely.`,\n );\n}\n"]}
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AuthProvider, Principal } from './types.js';
|
|
2
|
+
export interface Gate {
|
|
3
|
+
/** Is access allowed? true if there's no provider (opt-in: gate not set up → open; see makeGate in production). */
|
|
4
|
+
allow(req: Request, action: 'read' | 'write', resource?: string): Promise<boolean>;
|
|
5
|
+
/**
|
|
6
|
+
* Fine-grained variant of allow(): checks a SPECIFIC permission (e.g. 'agents:run', 'users:write').
|
|
7
|
+
* • No provider (auth off) → true (unchanged opt-in behavior).
|
|
8
|
+
* • EE / RBAC provider → the permission is passed through `AuthContext.permission` and matched against
|
|
9
|
+
* the principal's EFFECTIVE permissions (explicit `permissions[]` ?? role grants) — see rbac.ts.
|
|
10
|
+
* • Free / read-write provider → the permission is REDUCED to read/write (`:read` suffix → read, else
|
|
11
|
+
* write) and evaluated coarsely. This makes `allowP(req,'X:write') ≡ allow(req,'write')` and
|
|
12
|
+
* `allowP(req,'*:read') ≡ allow(req,'read')` in the free tier → no regression.
|
|
13
|
+
* A denial records its decision against the request (like allow) so `deny()` can surface status/reason.
|
|
14
|
+
*/
|
|
15
|
+
allowP(req: Request, permission: string): Promise<boolean>;
|
|
16
|
+
/** Return the denial. If allow() attached its last decision to the context, use its status/reason. */
|
|
17
|
+
deny(req: Request, action: 'read' | 'write'): Response;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The principal authenticated during allow() (within the same request). Hosts derive the organization
|
|
21
|
+
* scope and audit actor from here → closed to header spoofing. Null if allow() hasn't been called yet.
|
|
22
|
+
*/
|
|
23
|
+
export declare function principalOf(req: Request): Principal | null;
|
|
24
|
+
export interface GateOptions {
|
|
25
|
+
/**
|
|
26
|
+
* DELIBERATE permission for a providerless gate in production. Auth stays opt-in; but silent
|
|
27
|
+
* fail-open is impossible under NODE_ENV=production — either a provider is given or this flag is
|
|
28
|
+
* explicitly set to true (audit #2).
|
|
29
|
+
*/
|
|
30
|
+
allowOpenAccess?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function makeGate(provider?: AuthProvider, opts?: GateOptions): Gate;
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isCrossSiteStateChange } from './same-site.js';
|
|
2
|
+
import { bindsIdentity } from './adapter.js';
|
|
3
|
+
// Per-request state, keyed by the request itself rather than stashed as a property on it. A Request
|
|
4
|
+
// is somebody else's object; writing hidden fields onto it worked, but it also meant two libraries
|
|
5
|
+
// could pick the same key. A WeakMap cannot collide and cannot leak — the entry dies with the request.
|
|
6
|
+
const principals = new WeakMap();
|
|
7
|
+
const decisions = new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* The principal authenticated during allow() (within the same request). Hosts derive the organization
|
|
10
|
+
* scope and audit actor from here → closed to header spoofing. Null if allow() hasn't been called yet.
|
|
11
|
+
*/
|
|
12
|
+
export function principalOf(req) {
|
|
13
|
+
return principals.get(req) ?? null;
|
|
14
|
+
}
|
|
15
|
+
export function makeGate(provider, opts) {
|
|
16
|
+
// Fail-open audit at SETUP time: in production, a providerless gate can only be set up with the deliberate flag.
|
|
17
|
+
// (If left to request time, the error would blow up on the first request after deploy — an early, clear failure was preferred.)
|
|
18
|
+
if (!provider && process.env.NODE_ENV === 'production' && opts?.allowOpenAccess !== true) {
|
|
19
|
+
throw new Error('auth is required in production; for deliberately open access, set allowOpenAccess: true (see @gnldev/auth makeGate / host options)');
|
|
20
|
+
}
|
|
21
|
+
// Non-production providerless gate: warn ONCE on the first request (no silent openness), then open.
|
|
22
|
+
let warnedOpen = false;
|
|
23
|
+
let warnedCrossSite = false;
|
|
24
|
+
/**
|
|
25
|
+
* The providerless decision, in ONE place.
|
|
26
|
+
*
|
|
27
|
+
* It was written inline in `allow()` only, so `allowP()` — the fine-grained entry point, and the one
|
|
28
|
+
* 23 endpoints across @gnldev/server and @gnldev/studio actually call — kept an unconditional
|
|
29
|
+
* `return true`. Which meant the cross-site block covered the coarse path and left the specific one
|
|
30
|
+
* open: a page on another site could still drive every endpoint that asks for a named permission.
|
|
31
|
+
* Two copies of a security decision is one copy too many.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Is this request a cross-site state change against a surface with NO IDENTITY to ride?
|
|
35
|
+
*
|
|
36
|
+
* The condition is about identity, not about the presence of a provider — and getting that wrong
|
|
37
|
+
* left a hole. A legacy `{read,write}` pair IS a provider after normalizeAuth, so keying on
|
|
38
|
+
* `!provider` skipped the check for it; but that pair has no principal model at all (its
|
|
39
|
+
* authenticate() is `return null` by construction), so there is no credential a cross-site page
|
|
40
|
+
* could be riding, and its predicate answers on the request alone. Measured: with
|
|
41
|
+
* `{read:()=>true, write:()=>true}` configured, a cross-site `POST /api/retention/sweep` was
|
|
42
|
+
* ALLOWED. examples/app ships exactly that shape, so it was the published example that was open.
|
|
43
|
+
*
|
|
44
|
+
* `bindsIdentity(undefined)` is false, so the providerless case is covered by the same test.
|
|
45
|
+
*/
|
|
46
|
+
function crossSiteWithoutIdentity(req) {
|
|
47
|
+
if (bindsIdentity(provider) || !isCrossSiteStateChange(req))
|
|
48
|
+
return false;
|
|
49
|
+
if (!warnedCrossSite) {
|
|
50
|
+
warnedCrossSite = true;
|
|
51
|
+
console.warn('@gnldev/auth: blocked a cross-site write to a surface with no identity to authenticate ' +
|
|
52
|
+
'against. A page on another site attempted a state-changing request. Configure an auth ' +
|
|
53
|
+
'provider that binds an identity if this surface is meant to be reachable by other origins.');
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
function openSurfaceAllows(req) {
|
|
58
|
+
// The warning is an INSTRUCTION ("for deliberate open access use allowOpenAccess: true"), so it
|
|
59
|
+
// must stop once the instruction has been followed. It did not: the flag suppressed the
|
|
60
|
+
// production throw but not this line, so a developer who set it kept being told to set it — which
|
|
61
|
+
// teaches that the flag is inert and that this package's auth warnings can be ignored. Under
|
|
62
|
+
// NODE_ENV=production the same flag is already accepted as the whole declaration of intent; there
|
|
63
|
+
// is no reason for dev to demand it twice and then not honour it.
|
|
64
|
+
//
|
|
65
|
+
// Openness WITHOUT the flag still warns, every process, exactly as before — that is the case the
|
|
66
|
+
// message was written for.
|
|
67
|
+
if (!warnedOpen && opts?.allowOpenAccess !== true && process.env.NODE_ENV !== 'production') {
|
|
68
|
+
warnedOpen = true;
|
|
69
|
+
console.warn('@gnldev/auth: no provider given → ALL endpoints are open (opt-in gate not set up). Add auth before production; for deliberate open access use allowOpenAccess: true.');
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
async allow(req, action, resource) {
|
|
75
|
+
if (crossSiteWithoutIdentity(req))
|
|
76
|
+
return false;
|
|
77
|
+
if (!provider)
|
|
78
|
+
return openSurfaceAllows(req);
|
|
79
|
+
const principal = await provider.authenticate(req);
|
|
80
|
+
if (principal)
|
|
81
|
+
principals.set(req, principal);
|
|
82
|
+
const decision = await provider.authorize(principal, req, {
|
|
83
|
+
path: new URL(req.url).pathname,
|
|
84
|
+
method: req.method,
|
|
85
|
+
action,
|
|
86
|
+
resource,
|
|
87
|
+
});
|
|
88
|
+
if (!decision.allow)
|
|
89
|
+
decisions.set(req, decision);
|
|
90
|
+
return decision.allow;
|
|
91
|
+
},
|
|
92
|
+
async allowP(req, permission) {
|
|
93
|
+
if (crossSiteWithoutIdentity(req))
|
|
94
|
+
return false;
|
|
95
|
+
if (!provider)
|
|
96
|
+
return openSurfaceAllows(req);
|
|
97
|
+
const principal = await provider.authenticate(req);
|
|
98
|
+
if (principal)
|
|
99
|
+
principals.set(req, principal);
|
|
100
|
+
// Free-tier reduction: anything ending in ':read' is a read, everything else is a write. An RBAC
|
|
101
|
+
// provider ignores `action` and matches `permission` exactly; a free provider uses this reduced action.
|
|
102
|
+
const action = permission.endsWith(':read') ? 'read' : 'write';
|
|
103
|
+
const decision = await provider.authorize(principal, req, {
|
|
104
|
+
path: new URL(req.url).pathname,
|
|
105
|
+
method: req.method,
|
|
106
|
+
action,
|
|
107
|
+
permission,
|
|
108
|
+
});
|
|
109
|
+
if (!decision.allow)
|
|
110
|
+
decisions.set(req, decision);
|
|
111
|
+
return decision.allow;
|
|
112
|
+
},
|
|
113
|
+
deny(req, action) {
|
|
114
|
+
const d = decisions.get(req);
|
|
115
|
+
const denied = d && d.allow === false ? d : undefined;
|
|
116
|
+
const status = denied?.status ?? (action === 'write' ? 403 : 401);
|
|
117
|
+
const reason = denied?.reason ?? (action === 'write' ? 'unauthorized (admin required)' : 'unauthorized');
|
|
118
|
+
return Response.json({ error: reason }, { status });
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=gate.js.map
|