tina4-nodejs 3.13.105 → 3.13.108
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/CLAUDE.md +2 -2
- package/README.md +3 -4
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +76 -5
- package/packages/core/dist/index.js +76 -5
- package/packages/core/public/js/tina4js.min.js +3 -3
- package/packages/core/src/authGate.ts +61 -1
- package/packages/core/src/router.ts +38 -0
- package/packages/core/src/server.ts +29 -1
- package/packages/core/src/types.ts +4 -0
- package/packages/orm/dist/index.js +76 -5
- package/types/core/src/authGate.d.ts +3 -0
- package/types/core/src/router.d.ts +16 -0
- package/types/core/src/types.d.ts +4 -0
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.108)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.108 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
package/README.md
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
<h1 align="center">Tina4 Node.js</h1>
|
|
5
5
|
<h3 align="center">The Intelligent Native Application 4ramework</h3>
|
|
6
|
-
<p align="center">
|
|
6
|
+
<p align="center">Zero dependencies. One import, everything works.</p>
|
|
7
7
|
<p align="center">
|
|
8
|
-
<a href="https://
|
|
9
|
-
<img src="https://img.shields.io/
|
|
10
|
-
<img src="https://img.shields.io/badge/features-98-blue" alt="Features">
|
|
8
|
+
<a href="https://github.com/tina4stack/tina4-nodejs/releases"><img src="https://img.shields.io/github/v/tag/tina4stack/tina4-nodejs?color=7b1fa2&label=version&sort=semver" alt="version"></a>
|
|
9
|
+
<a href="https://github.com/tina4stack/tina4-nodejs/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/tina4stack/tina4-nodejs/test.yml?label=tests" alt="Tests"></a>
|
|
11
10
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
12
11
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
13
12
|
</p>
|
package/package.json
CHANGED
package/packages/cli/dist/bin.js
CHANGED
|
@@ -19195,6 +19195,31 @@ var init_router = __esm({
|
|
|
19195
19195
|
this.route.noAuth = true;
|
|
19196
19196
|
return this;
|
|
19197
19197
|
}
|
|
19198
|
+
/**
|
|
19199
|
+
* RBAC: require ONE of the named roles (OR). Reads the verified JWT `roles`
|
|
19200
|
+
* claim. Chain .role()/.can() for AND. Implies auth. Feature 138 / ADR-0058.
|
|
19201
|
+
*/
|
|
19202
|
+
role(...names) {
|
|
19203
|
+
const clean = names.filter((n) => n !== "");
|
|
19204
|
+
if (clean.length > 0) {
|
|
19205
|
+
(this.route.requiredRoles ??= []).push(clean);
|
|
19206
|
+
this.route.secure = true;
|
|
19207
|
+
}
|
|
19208
|
+
return this;
|
|
19209
|
+
}
|
|
19210
|
+
/**
|
|
19211
|
+
* RBAC: require ONE of the named permissions (OR). Reads the verified JWT
|
|
19212
|
+
* `permissions` claim; granted-side wildcards (`posts.*`, `*`) satisfy a
|
|
19213
|
+
* concrete requirement. Chain for AND. Implies auth. Feature 138.
|
|
19214
|
+
*/
|
|
19215
|
+
can(...permissions) {
|
|
19216
|
+
const clean = permissions.filter((p) => p !== "");
|
|
19217
|
+
if (clean.length > 0) {
|
|
19218
|
+
(this.route.requiredPerms ??= []).push(clean);
|
|
19219
|
+
this.route.secure = true;
|
|
19220
|
+
}
|
|
19221
|
+
return this;
|
|
19222
|
+
}
|
|
19198
19223
|
/** Mark this route's response as cacheable. */
|
|
19199
19224
|
cache() {
|
|
19200
19225
|
this.route.cached = true;
|
|
@@ -19262,7 +19287,9 @@ var init_router = __esm({
|
|
|
19262
19287
|
secure: secureDefault,
|
|
19263
19288
|
cached: definition.cached,
|
|
19264
19289
|
noAuth: definition.noAuth,
|
|
19265
|
-
template: definition.template
|
|
19290
|
+
template: definition.template,
|
|
19291
|
+
requiredRoles: definition.requiredRoles,
|
|
19292
|
+
requiredPerms: definition.requiredPerms
|
|
19266
19293
|
};
|
|
19267
19294
|
routes.push(compiled);
|
|
19268
19295
|
return new RouteRef(compiled);
|
|
@@ -19409,7 +19436,9 @@ var init_router = __esm({
|
|
|
19409
19436
|
template: route.template,
|
|
19410
19437
|
secure: route.secure,
|
|
19411
19438
|
cached: route.cached,
|
|
19412
|
-
noAuth: route.noAuth
|
|
19439
|
+
noAuth: route.noAuth,
|
|
19440
|
+
requiredRoles: route.requiredRoles,
|
|
19441
|
+
requiredPerms: route.requiredPerms
|
|
19413
19442
|
};
|
|
19414
19443
|
}
|
|
19415
19444
|
}
|
|
@@ -19432,7 +19461,9 @@ var init_router = __esm({
|
|
|
19432
19461
|
template: route.template,
|
|
19433
19462
|
secure: route.secure,
|
|
19434
19463
|
cached: route.cached,
|
|
19435
|
-
noAuth: route.noAuth
|
|
19464
|
+
noAuth: route.noAuth,
|
|
19465
|
+
requiredRoles: route.requiredRoles,
|
|
19466
|
+
requiredPerms: route.requiredPerms
|
|
19436
19467
|
});
|
|
19437
19468
|
}
|
|
19438
19469
|
}
|
|
@@ -19794,7 +19825,7 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
19794
19825
|
const identity = sso?.identity;
|
|
19795
19826
|
if (identity?.issuer && identity?.subject) {
|
|
19796
19827
|
req2.user = identity;
|
|
19797
|
-
return
|
|
19828
|
+
return rbacForbidden(match, identity, res);
|
|
19798
19829
|
}
|
|
19799
19830
|
const sessionToken = req2.session?.get?.("token");
|
|
19800
19831
|
if (sessionToken && validToken(sessionToken)) {
|
|
@@ -19814,8 +19845,45 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
19814
19845
|
res.header("FreshToken", fresh);
|
|
19815
19846
|
}
|
|
19816
19847
|
}
|
|
19848
|
+
return rbacForbidden(match, req2.user, res);
|
|
19849
|
+
}
|
|
19850
|
+
function rbacClaimList(subject, key, legacy) {
|
|
19851
|
+
const coerce = (v) => {
|
|
19852
|
+
if (typeof v === "string") return v === "" ? [] : [v];
|
|
19853
|
+
if (Array.isArray(v)) return v.map((x) => String(x)).filter((x) => x !== "");
|
|
19854
|
+
return [];
|
|
19855
|
+
};
|
|
19856
|
+
let out = coerce(subject[key]);
|
|
19857
|
+
if (out.length === 0 && legacy) out = coerce(subject[legacy]);
|
|
19858
|
+
return out;
|
|
19859
|
+
}
|
|
19860
|
+
function rbacPermGranted(granted, required) {
|
|
19861
|
+
return granted.some(
|
|
19862
|
+
(g) => g === "*" || g === required || g.endsWith(".*") && required.startsWith(g.slice(0, -1))
|
|
19863
|
+
);
|
|
19864
|
+
}
|
|
19865
|
+
function rbacForbidden(match, payload, res) {
|
|
19866
|
+
const requiredRoles = match.requiredRoles ?? [];
|
|
19867
|
+
const requiredPerms = match.requiredPerms ?? [];
|
|
19868
|
+
if (requiredRoles.length === 0 && requiredPerms.length === 0) {
|
|
19869
|
+
return false;
|
|
19870
|
+
}
|
|
19871
|
+
const subject = payload && typeof payload === "object" ? payload : {};
|
|
19872
|
+
const roles = rbacClaimList(subject, "roles", "role");
|
|
19873
|
+
for (const group of requiredRoles) {
|
|
19874
|
+
if (!group.some((r) => roles.includes(r))) return writeForbidden(res);
|
|
19875
|
+
}
|
|
19876
|
+
const perms = rbacClaimList(subject, "permissions");
|
|
19877
|
+
for (const group of requiredPerms) {
|
|
19878
|
+
if (!group.some((p) => rbacPermGranted(perms, p))) return writeForbidden(res);
|
|
19879
|
+
}
|
|
19817
19880
|
return false;
|
|
19818
19881
|
}
|
|
19882
|
+
function writeForbidden(res) {
|
|
19883
|
+
res.raw.writeHead(403, { "Content-Type": "application/json" });
|
|
19884
|
+
res.raw.end(JSON.stringify({ error: "Forbidden" }));
|
|
19885
|
+
return true;
|
|
19886
|
+
}
|
|
19819
19887
|
var init_authGate = __esm({
|
|
19820
19888
|
"../core/src/authGate.ts"() {
|
|
19821
19889
|
"use strict";
|
|
@@ -35912,6 +35980,9 @@ function asHtmlString(chunk) {
|
|
|
35912
35980
|
if (Buffer.isBuffer(chunk)) return chunk.toString("utf-8");
|
|
35913
35981
|
return null;
|
|
35914
35982
|
}
|
|
35983
|
+
function isInjectableHtml(res) {
|
|
35984
|
+
return isHtmlResponse(res) && !res.raw.getHeader("content-encoding");
|
|
35985
|
+
}
|
|
35915
35986
|
function injectIntoHtml(ctx, devToolbar, html) {
|
|
35916
35987
|
if (!devToolbar) return injectFeedbackWidget(ctx.req, html);
|
|
35917
35988
|
const toolbarCtx = {
|
|
@@ -35939,7 +36010,7 @@ function wrapResponseEnd(ctx) {
|
|
|
35939
36010
|
Date.now() - ctx.reqStartTime
|
|
35940
36011
|
);
|
|
35941
36012
|
}
|
|
35942
|
-
if (
|
|
36013
|
+
if (isInjectableHtml(res)) {
|
|
35943
36014
|
const html = asHtmlString(chunk);
|
|
35944
36015
|
if (html !== null) chunk = injectIntoHtml(ctx, devToolbar, html);
|
|
35945
36016
|
if (!res.raw.headersSent) res.raw.removeHeader("content-length");
|
|
@@ -19194,6 +19194,31 @@ var init_router = __esm({
|
|
|
19194
19194
|
this.route.noAuth = true;
|
|
19195
19195
|
return this;
|
|
19196
19196
|
}
|
|
19197
|
+
/**
|
|
19198
|
+
* RBAC: require ONE of the named roles (OR). Reads the verified JWT `roles`
|
|
19199
|
+
* claim. Chain .role()/.can() for AND. Implies auth. Feature 138 / ADR-0058.
|
|
19200
|
+
*/
|
|
19201
|
+
role(...names) {
|
|
19202
|
+
const clean = names.filter((n) => n !== "");
|
|
19203
|
+
if (clean.length > 0) {
|
|
19204
|
+
(this.route.requiredRoles ??= []).push(clean);
|
|
19205
|
+
this.route.secure = true;
|
|
19206
|
+
}
|
|
19207
|
+
return this;
|
|
19208
|
+
}
|
|
19209
|
+
/**
|
|
19210
|
+
* RBAC: require ONE of the named permissions (OR). Reads the verified JWT
|
|
19211
|
+
* `permissions` claim; granted-side wildcards (`posts.*`, `*`) satisfy a
|
|
19212
|
+
* concrete requirement. Chain for AND. Implies auth. Feature 138.
|
|
19213
|
+
*/
|
|
19214
|
+
can(...permissions) {
|
|
19215
|
+
const clean = permissions.filter((p) => p !== "");
|
|
19216
|
+
if (clean.length > 0) {
|
|
19217
|
+
(this.route.requiredPerms ??= []).push(clean);
|
|
19218
|
+
this.route.secure = true;
|
|
19219
|
+
}
|
|
19220
|
+
return this;
|
|
19221
|
+
}
|
|
19197
19222
|
/** Mark this route's response as cacheable. */
|
|
19198
19223
|
cache() {
|
|
19199
19224
|
this.route.cached = true;
|
|
@@ -19261,7 +19286,9 @@ var init_router = __esm({
|
|
|
19261
19286
|
secure: secureDefault,
|
|
19262
19287
|
cached: definition.cached,
|
|
19263
19288
|
noAuth: definition.noAuth,
|
|
19264
|
-
template: definition.template
|
|
19289
|
+
template: definition.template,
|
|
19290
|
+
requiredRoles: definition.requiredRoles,
|
|
19291
|
+
requiredPerms: definition.requiredPerms
|
|
19265
19292
|
};
|
|
19266
19293
|
routes.push(compiled);
|
|
19267
19294
|
return new RouteRef(compiled);
|
|
@@ -19408,7 +19435,9 @@ var init_router = __esm({
|
|
|
19408
19435
|
template: route.template,
|
|
19409
19436
|
secure: route.secure,
|
|
19410
19437
|
cached: route.cached,
|
|
19411
|
-
noAuth: route.noAuth
|
|
19438
|
+
noAuth: route.noAuth,
|
|
19439
|
+
requiredRoles: route.requiredRoles,
|
|
19440
|
+
requiredPerms: route.requiredPerms
|
|
19412
19441
|
};
|
|
19413
19442
|
}
|
|
19414
19443
|
}
|
|
@@ -19431,7 +19460,9 @@ var init_router = __esm({
|
|
|
19431
19460
|
template: route.template,
|
|
19432
19461
|
secure: route.secure,
|
|
19433
19462
|
cached: route.cached,
|
|
19434
|
-
noAuth: route.noAuth
|
|
19463
|
+
noAuth: route.noAuth,
|
|
19464
|
+
requiredRoles: route.requiredRoles,
|
|
19465
|
+
requiredPerms: route.requiredPerms
|
|
19435
19466
|
});
|
|
19436
19467
|
}
|
|
19437
19468
|
}
|
|
@@ -19793,7 +19824,7 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
19793
19824
|
const identity = sso?.identity;
|
|
19794
19825
|
if (identity?.issuer && identity?.subject) {
|
|
19795
19826
|
req2.user = identity;
|
|
19796
|
-
return
|
|
19827
|
+
return rbacForbidden(match, identity, res);
|
|
19797
19828
|
}
|
|
19798
19829
|
const sessionToken = req2.session?.get?.("token");
|
|
19799
19830
|
if (sessionToken && validToken(sessionToken)) {
|
|
@@ -19813,8 +19844,45 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
19813
19844
|
res.header("FreshToken", fresh);
|
|
19814
19845
|
}
|
|
19815
19846
|
}
|
|
19847
|
+
return rbacForbidden(match, req2.user, res);
|
|
19848
|
+
}
|
|
19849
|
+
function rbacClaimList(subject, key, legacy) {
|
|
19850
|
+
const coerce = (v) => {
|
|
19851
|
+
if (typeof v === "string") return v === "" ? [] : [v];
|
|
19852
|
+
if (Array.isArray(v)) return v.map((x) => String(x)).filter((x) => x !== "");
|
|
19853
|
+
return [];
|
|
19854
|
+
};
|
|
19855
|
+
let out = coerce(subject[key]);
|
|
19856
|
+
if (out.length === 0 && legacy) out = coerce(subject[legacy]);
|
|
19857
|
+
return out;
|
|
19858
|
+
}
|
|
19859
|
+
function rbacPermGranted(granted, required) {
|
|
19860
|
+
return granted.some(
|
|
19861
|
+
(g) => g === "*" || g === required || g.endsWith(".*") && required.startsWith(g.slice(0, -1))
|
|
19862
|
+
);
|
|
19863
|
+
}
|
|
19864
|
+
function rbacForbidden(match, payload, res) {
|
|
19865
|
+
const requiredRoles = match.requiredRoles ?? [];
|
|
19866
|
+
const requiredPerms = match.requiredPerms ?? [];
|
|
19867
|
+
if (requiredRoles.length === 0 && requiredPerms.length === 0) {
|
|
19868
|
+
return false;
|
|
19869
|
+
}
|
|
19870
|
+
const subject = payload && typeof payload === "object" ? payload : {};
|
|
19871
|
+
const roles = rbacClaimList(subject, "roles", "role");
|
|
19872
|
+
for (const group of requiredRoles) {
|
|
19873
|
+
if (!group.some((r) => roles.includes(r))) return writeForbidden(res);
|
|
19874
|
+
}
|
|
19875
|
+
const perms = rbacClaimList(subject, "permissions");
|
|
19876
|
+
for (const group of requiredPerms) {
|
|
19877
|
+
if (!group.some((p) => rbacPermGranted(perms, p))) return writeForbidden(res);
|
|
19878
|
+
}
|
|
19816
19879
|
return false;
|
|
19817
19880
|
}
|
|
19881
|
+
function writeForbidden(res) {
|
|
19882
|
+
res.raw.writeHead(403, { "Content-Type": "application/json" });
|
|
19883
|
+
res.raw.end(JSON.stringify({ error: "Forbidden" }));
|
|
19884
|
+
return true;
|
|
19885
|
+
}
|
|
19818
19886
|
var init_authGate = __esm({
|
|
19819
19887
|
"src/authGate.ts"() {
|
|
19820
19888
|
"use strict";
|
|
@@ -35891,6 +35959,9 @@ function asHtmlString(chunk) {
|
|
|
35891
35959
|
if (Buffer.isBuffer(chunk)) return chunk.toString("utf-8");
|
|
35892
35960
|
return null;
|
|
35893
35961
|
}
|
|
35962
|
+
function isInjectableHtml(res) {
|
|
35963
|
+
return isHtmlResponse(res) && !res.raw.getHeader("content-encoding");
|
|
35964
|
+
}
|
|
35894
35965
|
function injectIntoHtml(ctx, devToolbar, html) {
|
|
35895
35966
|
if (!devToolbar) return injectFeedbackWidget(ctx.req, html);
|
|
35896
35967
|
const toolbarCtx = {
|
|
@@ -35918,7 +35989,7 @@ function wrapResponseEnd(ctx) {
|
|
|
35918
35989
|
Date.now() - ctx.reqStartTime
|
|
35919
35990
|
);
|
|
35920
35991
|
}
|
|
35921
|
-
if (
|
|
35992
|
+
if (isInjectableHtml(res)) {
|
|
35922
35993
|
const html = asHtmlString(chunk);
|
|
35923
35994
|
if (html !== null) chunk = injectIntoHtml(ctx, devToolbar, html);
|
|
35924
35995
|
if (!res.raw.headersSent) res.raw.removeHeader("content-length");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var Tina4=(()=>{var X=Object.defineProperty;var Be=Object.getOwnPropertyDescriptor;var Ve=Object.getOwnPropertyNames;var Ge=Object.prototype.hasOwnProperty;var Ze=(e,n)=>{for(var t in n)X(e,t,{get:n[t],enumerable:!0})},Qe=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of Ve(n))!Ge.call(e,o)&&o!==t&&X(e,o,{get:()=>n[o],enumerable:!(r=Be(n,o))||r.enumerable});return e};var Xe=e=>Qe(X({},"__esModule",{value:!0}),e);var Et={};Ze(Et,{Tina4Element:()=>P,api:()=>_e,batch:()=>V,clearPersistedKeys:()=>je,computed:()=>me,createI18n:()=>le,effect:()=>R,html:()=>ve,i18n:()=>We,isSignal:()=>I,navigate:()=>G,persist:()=>Ue,pwa:()=>Me,route:()=>Te,router:()=>Ce,rtc:()=>Ie,rtcConfig:()=>Z,signal:()=>k,sse:()=>Oe,ws:()=>W});var L=null,q=null,$=null,J=null;function O(e){J=e}function B(){return J}var fe=null,ge=null,pe=[],Ye=512;var z=0,Y=new Set;function k(e,n){let t=e,r=new Set,o={_t4:!0,get value(){if(L&&(r.add(L),q)){let s=L;q.push(()=>r.delete(s))}return t},set value(s){if(Object.is(s,t))return;let i=t;if(t=s,o._debugInfo&&o._debugInfo.updateCount++,ge&&ge(o,i,s),z>0)for(let l of r)Y.add(l);else{let l;for(let d of[...r])try{d()}catch(f){l===void 0&&(l=f)}if(l!==void 0)throw l}},_subscribe(s){return r.add(s),()=>{r.delete(s)}},peek(){return t}};return fe?(o._debugInfo={label:n,createdAt:Date.now(),updateCount:0,subs:r},fe(o,n)):pe.length<Ye&&pe.push({ref:new WeakRef(o),label:n,createdAt:Date.now(),subs:r}),o}function me(e){let n=k(void 0);return R(()=>{n.value=e()}),{_t4:!0,get value(){return n.value},set value(t){throw new Error("[tina4] computed signals are read-only")},_subscribe(t){return n._subscribe(t)},peek(){return n.peek()}}}function R(e){let n=!1,t=[],r=[],o=()=>{for(let l of r)l();r=[]},s=()=>{if(n)return;for(let h of t)h();t=[],o();let l=L,d=q,f=$;L=s,q=t,$=r;try{e()}finally{L=l,q=d,$=f}};s();let i=()=>{n=!0;for(let l of t)l();t=[],o()};return $&&$.push(i),J&&J.push(i),i}function V(e){z++;try{e()}finally{if(z--,z===0){let n=[...Y];Y.clear();let t;for(let r of n)try{r()}catch(o){t===void 0&&(t=o)}if(t!==void 0)throw t}}}function I(e){return e!==null&&typeof e=="object"&&e._t4===!0}var he=new WeakMap,ee="t4:";function ve(e,...n){let t=he.get(e);if(!t){t=document.createElement("template");let i="";for(let l=0;l<e.length;l++)i+=e[l],l<n.length&&(ot(i)?i+=`__t4_${l}__`:i+=`<!--${ee}${l}-->`);t.innerHTML=i,he.set(e,t)}let r=t.content.cloneNode(!0),o=et(r);for(let{marker:i,index:l}of o)nt(i,n[l]);let s=tt(r);for(let i of s)rt(i,n);return r}function et(e){let n=[];return ne(e,t=>{if(t.nodeType===8){let r=t.data;if(r&&r.startsWith(ee)){let o=parseInt(r.slice(ee.length),10);n.push({marker:t,index:o})}}}),n}function tt(e){let n=[];return ne(e,t=>{t.nodeType===1&&n.push(t)}),n}function ne(e,n){let t=e.childNodes;for(let r=0;r<t.length;r++){let o=t[r];n(o),ne(o,n)}}function nt(e,n){let t=e.parentNode;if(t)if(I(n)){let r=document.createTextNode("");t.replaceChild(r,e),R(()=>{r.data=String(n.value??"")})}else if(typeof n=="function"){let r=document.createComment("");t.replaceChild(r,e);let o=[],s=[];R(()=>{for(let w of s)w();s=[];let i=[],l=B();O(i);let d=n();O(l),s=i;for(let w of o)w.parentNode?.removeChild(w);o=[];let f=te(d),h=r.parentNode;if(h)for(let w of f)h.insertBefore(w,r),o.push(w)})}else if(ye(n))t.replaceChild(n,e);else if(n instanceof Node)t.replaceChild(n,e);else if(Array.isArray(n)){let r=document.createDocumentFragment();for(let o of n){let s=te(o);for(let i of s)r.appendChild(i)}t.replaceChild(r,e)}else{let r=document.createTextNode(String(n??""));t.replaceChild(r,e)}}function rt(e,n){let t=[];for(let r of Array.from(e.attributes)){let o=r.name,s=r.value;if(o.startsWith("@")){let l=o.slice(1),d=s.match(/__t4_(\d+)__/);if(d){let f=n[parseInt(d[1],10)];typeof f=="function"&&e.addEventListener(l,h=>V(()=>f(h)))}t.push(o);continue}if(o.startsWith("?")){let l=o.slice(1),d=s.match(/__t4_(\d+)__/);if(d){let f=n[parseInt(d[1],10)];if(I(f)){let h=f;R(()=>{h.value?e.setAttribute(l,""):e.removeAttribute(l)})}else typeof f=="function"?R(()=>{f()?e.setAttribute(l,""):e.removeAttribute(l)}):f&&e.setAttribute(l,"")}t.push(o);continue}if(o.startsWith(".")){let l=o.slice(1),d=s.match(/__t4_(\d+)__/);if(d){let f=n[parseInt(d[1],10)];I(f)?R(()=>{e[l]=f.value}):typeof f=="function"?R(()=>{e[l]=f()??""}):e[l]=f}t.push(o);continue}let i=s.match(/__t4_(\d+)__/);if(i){let l=n[parseInt(i[1],10)];if(I(l)){let d=l;R(()=>{e.setAttribute(o,String(d.value??""))})}else typeof l=="function"?R(()=>{e.setAttribute(o,String(l()??""))}):e.setAttribute(o,String(l??""))}}for(let r of t)e.removeAttribute(r)}function te(e){if(e==null||e===!1)return[];if(ye(e))return Array.from(e.childNodes);if(e instanceof Node)return[e];if(Array.isArray(e)){let n=[];for(let t of e)n.push(...te(t));return n}return[document.createTextNode(String(e))]}function ye(e){return e!=null&&typeof e=="object"&&e.nodeType===11}function ot(e){let n=!1,t=!1,r=!1;for(let o=0;o<e.length;o++){let s=e[o];s==="<"&&!n&&!t&&(r=!0),s===">"&&!n&&!t&&(r=!1),r&&(s==='"'&&!n&&(t=!t),s==="'"&&!t&&(n=!n))}return r}var be=null,Se=null;var P=class extends HTMLElement{constructor(){super();this._props={};this._rendered=!1;this._disposeRender=null;this._innerDisposers=[];let t=this.constructor;this._root=t.shadow?this.attachShadow({mode:"open"}):this;for(let[r,o]of Object.entries(t.props))this._props[r]=k(this._coerce(this.getAttribute(r),o))}static get observedAttributes(){return Object.keys(this.props)}connectedCallback(){if(this._rendered)return;this._rendered=!0;let t=this.constructor,r=null;if(t.styles&&t.shadow&&this._root instanceof ShadowRoot){let o=document.createElement("style");o.textContent=t.styles,this._root.appendChild(o),r=o}this._disposeRender=R(()=>{this._innerDisposers.splice(0).forEach(d=>d());let o=[],s=B();O(o);let i=this.render();O(s),this._innerDisposers=o;let l=Array.from(this._root.childNodes);for(let d of l)d!==r&&this._root.removeChild(d);i&&this._root.appendChild(i)}),this.onMount(),be&&be(this)}disconnectedCallback(){this._disposeRender&&(this._disposeRender(),this._disposeRender=null),this._innerDisposers.splice(0).forEach(t=>t()),this.onUnmount(),Se&&Se(this)}attributeChangedCallback(t,r,o){let i=this.constructor.props[t];i&&this._props[t]&&(this._props[t].value=this._coerce(o,i))}prop(t){if(!this._props[t])throw new Error(`[tina4] Prop '${t}' not declared in static props of <${this.tagName.toLowerCase()}>`);return this._props[t]}emit(t,r){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0,...r}))}onMount(){}onUnmount(){}_coerce(t,r){return r===Boolean?t!==null:r===Number?t!==null?Number(t):0:t??""}};P.props={},P.styles="",P.shadow=!0;var oe=[],D=null,U="history",st=!1,j=[],re=[],ke=0;function Te(e,n){let t=[],r;e==="*"?r=".*":r=e.replace(/\{(\w+)\}/g,(s,i)=>(t.push(i),"([^/]+)"));let o=new RegExp(`^${r}$`);typeof n=="function"?oe.push({pattern:e,regex:o,paramNames:t,handler:n}):oe.push({pattern:e,regex:o,paramNames:t,handler:n.handler,guard:n.guard})}function G(e,n){if(U==="hash")if(n?.replace){let t=new URL(location.href);t.hash="#"+e,history.replaceState(null,"",t.toString()),H()}else location.hash="#"+e;else n?.replace?history.replaceState(null,"",e):history.pushState(null,"",e),H()}function H(){if(!D)return;let e=performance.now(),n=++ke,t=U==="hash"?location.hash.slice(1)||"/":location.pathname;for(let r of oe){let o=t.match(r.regex);if(!o)continue;let s={};if(r.paramNames.forEach((d,f)=>{s[d]=decodeURIComponent(o[f+1])}),r.guard){let d=r.guard();if(d===!1)return;if(typeof d=="string"){G(d,{replace:!0});return}}re.splice(0).forEach(d=>d()),D.innerHTML="";let i=[];O(i);let l=r.handler(s);if(l instanceof Promise)l.then(d=>{if(O(null),n!==ke){for(let h of i)h();return}we(D,d),re=i;let f=performance.now()-e;for(let h of j)h({path:t,params:s,pattern:r.pattern,durationMs:f})});else{O(null),we(D,l),re=i;let d=performance.now()-e;for(let f of j)f({path:t,params:s,pattern:r.pattern,durationMs:d})}return}}function we(e,n){n instanceof DocumentFragment||n instanceof Node?e.replaceChildren(n):typeof n=="string"?e.innerHTML=n:n!=null&&e.replaceChildren(document.createTextNode(String(n)))}var Ce={start(e){if(D=document.querySelector(e.target),!D)throw new Error(`[tina4] Router target '${e.target}' not found in DOM`);U=e.mode??"history",st=!0,window.addEventListener("popstate",H),U==="hash"&&window.addEventListener("hashchange",H),document.addEventListener("click",n=>{if(n.metaKey||n.ctrlKey||n.shiftKey||n.altKey)return;let t=n.target.closest("a[href]");if(!t||t.origin!==location.origin||t.hasAttribute("target")||t.hasAttribute("download")||t.getAttribute("rel")?.includes("external"))return;n.preventDefault();let r=U==="hash"?t.getAttribute("href"):t.pathname;G(r)}),H()},on(e,n){return j.push(n),()=>{let t=j.indexOf(n);t>=0&&j.splice(t,1)}}};var x={baseUrl:"",auth:!1,tokenKey:"tina4_token",headers:{}},se=[],ie=[],it=0;function ae(){try{return localStorage.getItem(x.tokenKey)}catch{return null}}function at(e){try{localStorage.setItem(x.tokenKey,e)}catch{}}function Ee(e,n){let t=Object.entries(n).map(([r,o])=>`${encodeURIComponent(r)}=${encodeURIComponent(String(o))}`).join("&");return e+(e.includes("?")?"&":"?")+t}async function Re(e,n){e._url=n,e._requestId=++it;for(let l of se){let d=l(e);d&&(e=d)}let t=await fetch(n,e),r=t.headers.get("FreshToken");r&&at(r);let o=t.headers.get("Content-Type")??"",s;o.includes("json")?s=await t.json():s=await t.text();let i={status:t.status,data:s,ok:t.ok,headers:t.headers,_requestId:e._requestId};for(let l of ie){let d=l(i);d&&(i=d)}if(!t.ok)throw i;return i.data}async function F(e,n,t,r){let o={method:e,credentials:"same-origin",headers:{"Content-Type":"application/json",...x.headers}};if(x.auth){let s=ae();s&&(o.headers.Authorization=`Bearer ${s}`)}if(t!==void 0&&e!=="GET"){let s=typeof t=="object"&&t!==null?{...t}:t;if(x.auth&&typeof s=="object"&&s!==null){let i=ae();i&&(s.formToken=i)}o.body=JSON.stringify(s)}return r?.headers&&Object.assign(o.headers,r.headers),r?.params&&(n=Ee(n,r.params)),Re(o,x.baseUrl+n)}var _e={configure(e){Object.assign(x,e)},get(e,n){return F("GET",e,void 0,n)},post(e,n,t){return F("POST",e,n,t)},put(e,n,t){return F("PUT",e,n,t)},patch(e,n,t){return F("PATCH",e,n,t)},delete(e,n){return F("DELETE",e,void 0,n)},async graphql(e,n,t,r){return F("POST",e,{query:n,variables:t||{}},r)},async upload(e,n,t){let r={method:"POST",headers:{...x.headers},body:n};if(delete r.headers["Content-Type"],delete r.headers["content-type"],x.auth){let o=ae();o&&(r.headers.Authorization=`Bearer ${o}`)}return t?.headers&&Object.assign(r.headers,t.headers),t?.params&&(e=Ee(e,t.params)),Re(r,x.baseUrl+e)},intercept(e,n){e==="request"?se.push(n):ie.push(n)},_reset(){x.baseUrl="",x.auth=!1,x.tokenKey="tina4_token",x.headers={},se.length=0,ie.length=0}};function lt(e){let n=e.cacheStrategy??"network-first",t=JSON.stringify(e.precache??[]),r=e.offlineRoute?`'${e.offlineRoute}'`:"null";return`
|
|
1
|
+
"use strict";var Tina4=(()=>{var ee=Object.defineProperty;var Xe=Object.getOwnPropertyDescriptor;var Ye=Object.getOwnPropertyNames;var et=Object.prototype.hasOwnProperty;var tt=(e,n)=>{for(var t in n)ee(e,t,{get:n[t],enumerable:!0})},nt=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of Ye(n))!et.call(e,o)&&o!==t&&ee(e,o,{get:()=>n[o],enumerable:!(r=Xe(n,o))||r.enumerable});return e};var rt=e=>nt(ee({},"__esModule",{value:!0}),e);var $t={};tt($t,{Tina4Element:()=>N,api:()=>Ae,batch:()=>G,clearPersistedKeys:()=>Ve,computed:()=>ye,createI18n:()=>de,effect:()=>E,html:()=>be,i18n:()=>Je,isSignal:()=>I,navigate:()=>Q,persist:()=>ze,pwa:()=>Ne,route:()=>_e,router:()=>xe,rtc:()=>De,rtcConfig:()=>X,signal:()=>w,sse:()=>Pe,ws:()=>K});var L=null,H=null,U=null,B=null;function O(e){B=e}function J(){return B}var me=null,he=null,ve=[],ot=512;var V=0,te=new Set;function w(e,n){let t=e,r=new Set,o={_t4:!0,get value(){if(L&&(r.add(L),H)){let s=L;H.push(()=>r.delete(s))}return t},set value(s){if(Object.is(s,t))return;let i=t;if(t=s,o._debugInfo&&o._debugInfo.updateCount++,he&&he(o,i,s),V>0)for(let c of r)te.add(c);else{let c;for(let a of[...r])try{a()}catch(l){c===void 0&&(c=l)}if(c!==void 0)throw c}},_subscribe(s){return r.add(s),()=>{r.delete(s)}},peek(){return t}};return me?(o._debugInfo={label:n,createdAt:Date.now(),updateCount:0,subs:r},me(o,n)):ve.length<ot&&ve.push({ref:new WeakRef(o),label:n,createdAt:Date.now(),subs:r}),o}function ye(e){let n=w(void 0);return E(()=>{n.value=e()}),{_t4:!0,get value(){return n.value},set value(t){throw new Error("[tina4] computed signals are read-only")},_subscribe(t){return n._subscribe(t)},peek(){return n.peek()}}}function E(e){let n=!1,t=[],r=[],o=()=>{for(let c of r)c();r=[]},s=()=>{if(n)return;for(let g of t)g();t=[],o();let c=L,a=H,l=U;L=s,H=t,U=r;try{e()}finally{L=c,H=a,U=l}};s();let i=()=>{n=!0;for(let c of t)c();t=[],o()};return U&&U.push(i),B&&B.push(i),i}function G(e){V++;try{e()}finally{if(V--,V===0){let n=[...te];te.clear();let t;for(let r of n)try{r()}catch(o){t===void 0&&(t=o)}if(t!==void 0)throw t}}}function I(e){return e!==null&&typeof e=="object"&&e._t4===!0}var Se=new WeakMap,ne="t4:";function be(e,...n){let t=Se.get(e);if(!t){let i=document.createElement("template"),c=new Map,a="";for(let l=0;l<e.length;l++)if(a+=e[l],l<n.length)if(dt(a)){let m=ut(e[l]);m&&c.set(l,m),a+=`__t4_${l}__`}else a+=`<!--${ne}${l}-->`;i.innerHTML=a,t={template:i,propertyNames:c},Se.set(e,t)}let r=t.template.content.cloneNode(!0),o=st(r);for(let{marker:i,index:c}of o)at(i,n[c]);let s=it(r);for(let i of s)ct(i,n,t.propertyNames);return r}function st(e){let n=[];return se(e,t=>{if(t.nodeType===8){let r=t.data;if(r&&r.startsWith(ne)){let o=parseInt(r.slice(ne.length),10);n.push({marker:t,index:o})}}}),n}function it(e){let n=[];return se(e,t=>{t.nodeType===1&&n.push(t)}),n}function se(e,n){let t=e.childNodes;for(let r=0;r<t.length;r++){let o=t[r];n(o),se(o,n)}}function at(e,n){let t=e.parentNode;if(t)if(I(n)){let r=document.createTextNode("");t.replaceChild(r,e),E(()=>{r.data=String(n.value??"")})}else if(typeof n=="function"){let r=document.createComment("");t.replaceChild(r,e);let o=[],s=[];E(()=>{for(let u of s)u();s=[];let i=[],c=J();O(i);let a=n();O(c),s=i;for(let u of o)u.parentNode?.removeChild(u);o=[];let l=oe(a),g=r.parentNode;if(!g)return;let m=Z(g);for(let u of l){let d=m?D(u,m):u;g.insertBefore(d,r),o.push(d)}})}else if(Te(n)){let r=Z(t);if(r){let o=document.createDocumentFragment();for(let s of Array.from(n.childNodes))o.appendChild(D(s,r));t.replaceChild(o,e)}else t.replaceChild(n,e)}else if(n instanceof Node){let r=Z(t);t.replaceChild(r?D(n,r):n,e)}else if(Array.isArray(n)){let r=Z(t),o=document.createDocumentFragment();for(let s of n){let i=oe(s);for(let c of i)o.appendChild(r?D(c,r):c)}t.replaceChild(o,e)}else{let r=document.createTextNode(String(n??""));t.replaceChild(r,e)}}function ct(e,n,t){let r=[];for(let o of Array.from(e.attributes)){let s=o.name,i=o.value;if(s.startsWith("@")){let a=s.slice(1),l=i.match(/__t4_(\d+)__/);if(l){let g=n[parseInt(l[1],10)];typeof g=="function"&&e.addEventListener(a,m=>G(()=>g(m)))}r.push(s);continue}if(s.startsWith("?")){let a=s.slice(1),l=i.match(/__t4_(\d+)__/);if(l){let g=n[parseInt(l[1],10)];if(I(g)){let m=g;E(()=>{m.value?e.setAttribute(a,""):e.removeAttribute(a)})}else typeof g=="function"?E(()=>{g()?e.setAttribute(a,""):e.removeAttribute(a)}):g&&e.setAttribute(a,"")}r.push(s);continue}if(s.startsWith(".")){let a=i.match(/__t4_(\d+)__/);if(a){let l=parseInt(a[1],10),g=t.get(l)??s.slice(1),m=n[l];I(m)?E(()=>{e[g]=m.value}):typeof m=="function"?E(()=>{e[g]=m()??""}):e[g]=m}r.push(s);continue}let c=i.match(/__t4_(\d+)__/);if(c){let a=n[parseInt(c[1],10)];if(I(a)){let l=a;E(()=>{e.setAttribute(s,String(l.value??""))})}else typeof a=="function"?E(()=>{e.setAttribute(s,String(a()??""))}):e.setAttribute(s,String(a??""))}}for(let o of r)e.removeAttribute(o)}var re="http://www.w3.org/2000/svg",lt="http://www.w3.org/1998/Math/MathML",we="http://www.w3.org/1999/xhtml";function Z(e){let n=e;for(;n&&n.nodeType===1;){let t=n,r=t.namespaceURI;if(r===re&&t.localName==="foreignObject")return null;if(r===re||r===lt)return r;if(r===we)return null;n=n.parentNode}return null}function D(e,n){if(e.nodeType!==1)return e;let t=e;if(t.namespaceURI===n){for(let s of Array.from(t.childNodes)){let i=D(s,n);i!==s&&t.replaceChild(i,s)}return t}let r=document.createElementNS(n,t.localName);for(let s of Array.from(t.attributes))r.setAttribute(s.name,s.value);let o=n===re&&t.localName==="foreignObject"?we:n;for(let s of Array.from(t.childNodes))r.appendChild(D(s,o));return r}function ut(e){return e.match(/\.([^\s"'<>/=]+)\s*=\s*["']?$/)?.[1]}function oe(e){if(e==null||e===!1)return[];if(Te(e))return Array.from(e.childNodes);if(e instanceof Node)return[e];if(Array.isArray(e)){let n=[];for(let t of e)n.push(...oe(t));return n}return[document.createTextNode(String(e))]}function Te(e){return e!=null&&typeof e=="object"&&e.nodeType===11}function dt(e){let n=!1,t=!1,r=!1;for(let o=0;o<e.length;o++){if(!r&&e.startsWith("<!--",o)){let i=e.indexOf("-->",o+4);if(i===-1)return!1;o=i+2;continue}let s=e[o];s==="<"&&!n&&!t&&(r=!0),s===">"&&!n&&!t&&(r=!1),r&&(s==='"'&&!n&&(t=!t),s==="'"&&!t&&(n=!n))}return r}var ke=null,Ce=null;var N=class extends HTMLElement{constructor(){super();this._props={};this._rendered=!1;this._disposeRender=null;this._innerDisposers=[];let t=this.constructor;this._root=t.shadow?this.attachShadow({mode:"open"}):this;for(let[r,o]of Object.entries(t.props))this._props[r]=w(this._coerce(this.getAttribute(r),o))}static get observedAttributes(){return Object.keys(this.props)}connectedCallback(){if(this._rendered)return;this._rendered=!0;let t=this.constructor,r=null;if(t.styles&&t.shadow&&this._root instanceof ShadowRoot){let o=document.createElement("style");o.textContent=t.styles,this._root.appendChild(o),r=o}this._disposeRender=E(()=>{this._innerDisposers.splice(0).forEach(a=>a());let o=[],s=J();O(o);let i=this.render();O(s),this._innerDisposers=o;let c=Array.from(this._root.childNodes);for(let a of c)a!==r&&this._root.removeChild(a);i&&this._root.appendChild(i)}),this.onMount(),ke&&ke(this)}disconnectedCallback(){this._disposeRender&&(this._disposeRender(),this._disposeRender=null),this._innerDisposers.splice(0).forEach(t=>t()),this.onUnmount(),Ce&&Ce(this)}attributeChangedCallback(t,r,o){let i=this.constructor.props[t];i&&this._props[t]&&(this._props[t].value=this._coerce(o,i))}prop(t){if(!this._props[t])throw new Error(`[tina4] Prop '${t}' not declared in static props of <${this.tagName.toLowerCase()}>`);return this._props[t]}emit(t,r){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0,...r}))}onMount(){}onUnmount(){}_coerce(t,r){return r===Boolean?t!==null:r===Number?t!==null?Number(t):0:t??""}};N.props={},N.styles="",N.shadow=!0;var ae=[],F=null,j="history",ft=!1,W=[],ie=[],Ee=0;function _e(e,n){let t=[],r;e==="*"?r=".*":r=e.replace(/\{(\w+)\}/g,(s,i)=>(t.push(i),"([^/]+)"));let o=new RegExp(`^${r}$`);typeof n=="function"?ae.push({pattern:e,regex:o,paramNames:t,handler:n}):ae.push({pattern:e,regex:o,paramNames:t,handler:n.handler,guard:n.guard})}function Q(e,n){if(j==="hash")if(n?.replace){let t=new URL(location.href);t.hash="#"+e,history.replaceState(null,"",t.toString()),$()}else{let t=new URL(location.href);t.hash="#"+e,history.pushState(null,"",t.toString()),$()}else n?.replace?history.replaceState(null,"",e):history.pushState(null,"",e),$()}function $(){if(!F)return;let e=performance.now(),n=++Ee,t=j==="hash"?location.hash.slice(1)||"/":location.pathname;for(let r of ae){let o=t.match(r.regex);if(!o)continue;let s={};if(r.paramNames.forEach((a,l)=>{s[a]=decodeURIComponent(o[l+1])}),r.guard){let a=r.guard();if(a===!1)return;if(typeof a=="string"){Q(a,{replace:!0});return}}ie.splice(0).forEach(a=>a()),F.innerHTML="";let i=[];O(i);let c=r.handler(s);if(c instanceof Promise)c.then(a=>{if(O(null),n!==Ee){for(let g of i)g();return}Re(F,a),ie=i;let l=performance.now()-e;for(let g of W)g({path:t,params:s,pattern:r.pattern,durationMs:l})});else{O(null),Re(F,c),ie=i;let a=performance.now()-e;for(let l of W)l({path:t,params:s,pattern:r.pattern,durationMs:a})}return}}function Re(e,n){n instanceof DocumentFragment||n instanceof Node?e.replaceChildren(n):typeof n=="string"?e.innerHTML=n:n!=null&&e.replaceChildren(document.createTextNode(String(n)))}var xe={start(e){if(F=document.querySelector(e.target),!F)throw new Error(`[tina4] Router target '${e.target}' not found in DOM`);j=e.mode??"history",ft=!0,window.addEventListener("popstate",$),j==="hash"&&window.addEventListener("hashchange",$),document.addEventListener("click",n=>{if(n.metaKey||n.ctrlKey||n.shiftKey||n.altKey)return;let t=n.target.closest("a[href]");if(!t||t.origin!==location.origin||t.hasAttribute("target")||t.hasAttribute("download")||t.getAttribute("rel")?.includes("external"))return;n.preventDefault();let r=j==="hash"?t.getAttribute("href"):t.pathname;Q(r)}),$()},on(e,n){return W.push(n),()=>{let t=W.indexOf(n);t>=0&&W.splice(t,1)}}};var _={baseUrl:"",auth:!1,tokenKey:"tina4_token",headers:{}},ce=[],le=[],gt=0;function ue(){try{return localStorage.getItem(_.tokenKey)}catch{return null}}function pt(e){try{localStorage.setItem(_.tokenKey,e)}catch{}}function Me(e,n){let t=Object.entries(n).map(([r,o])=>`${encodeURIComponent(r)}=${encodeURIComponent(String(o))}`).join("&");return e+(e.includes("?")?"&":"?")+t}async function Oe(e,n){e._url=n,e._requestId=++gt;for(let c of ce){let a=c(e);a&&(e=a)}let t=await fetch(n,e),r=t.headers.get("FreshToken");r&&pt(r);let o=t.headers.get("Content-Type")??"",s;o.includes("json")?s=await t.json():s=await t.text();let i={status:t.status,data:s,ok:t.ok,headers:t.headers,_requestId:e._requestId};for(let c of le){let a=c(i);a&&(i=a)}if(!t.ok)throw i;return i.data}async function q(e,n,t,r){let o={method:e,credentials:"same-origin",headers:{"Content-Type":"application/json",..._.headers}};if(_.auth){let s=ue();s&&(o.headers.Authorization=`Bearer ${s}`)}if(t!==void 0&&e!=="GET"){let s=typeof t=="object"&&t!==null?{...t}:t;if(_.auth&&typeof s=="object"&&s!==null){let i=ue();i&&(s.formToken=i)}o.body=JSON.stringify(s)}return r?.headers&&Object.assign(o.headers,r.headers),r?.params&&(n=Me(n,r.params)),Oe(o,_.baseUrl+n)}var Ae={configure(e){Object.assign(_,e)},get(e,n){return q("GET",e,void 0,n)},post(e,n,t){return q("POST",e,n,t)},put(e,n,t){return q("PUT",e,n,t)},patch(e,n,t){return q("PATCH",e,n,t)},delete(e,n){return q("DELETE",e,void 0,n)},async graphql(e,n,t,r){return q("POST",e,{query:n,variables:t||{}},r)},async upload(e,n,t){let r={method:"POST",headers:{..._.headers},body:n};if(delete r.headers["Content-Type"],delete r.headers["content-type"],_.auth){let o=ue();o&&(r.headers.Authorization=`Bearer ${o}`)}return t?.headers&&Object.assign(r.headers,t.headers),t?.params&&(e=Me(e,t.params)),Oe(r,_.baseUrl+e)},intercept(e,n){e==="request"?ce.push(n):le.push(n)},_reset(){_.baseUrl="",_.auth=!1,_.tokenKey="tina4_token",_.headers={},ce.length=0,le.length=0}};function mt(e){let n=e.cacheStrategy??"network-first",t=JSON.stringify(e.precache??[]),r=e.offlineRoute?`'${e.offlineRoute}'`:"null";return`
|
|
2
2
|
const CACHE = 'tina4-v1';
|
|
3
3
|
const PRECACHE = ${t};
|
|
4
4
|
const OFFLINE = ${r};
|
|
@@ -44,5 +44,5 @@ self.addEventListener('fetch', (e) => {
|
|
|
44
44
|
))
|
|
45
45
|
);`}
|
|
46
46
|
});
|
|
47
|
-
`.trim()}function
|
|
48
|
-
`);N=ue.pop();for(let Je of ue){let de=Je.trim();de&&C(y(de),null)}}let ce=N.trim();ce&&C(y(ce),null),w=null,T()}).catch(b=>{b.name!=="AbortError"&&(w=null,_(b),T())})}function Q(){g++,d.value=g,r.value="reconnecting",c=setTimeout(()=>{c=null,S()},a),a=Math.min(a*2,t.reconnectMaxDelay)}function S(){t.mode==="fetch"?K():A()}let v={status:r,connected:o,lastMessage:s,lastEvent:i,error:l,reconnectCount:d,on(p,b){return f[p].push(b),()=>{let E=f[p],M=E.indexOf(b);M>=0&&E.splice(M,1)}},pipe(p,b){let E=M=>{p.value=b(M,p.value)};return v.on("message",E)},close(){u=!0,c&&(clearTimeout(c),c=null),h&&(h.close(),h=null),w&&(w.abort(),w=null),r.value="closed",o.value=!1}};return S(),v}var Oe={connect:gt};async function Z(e="/api/rtc/config"){let n=await fetch(e);if(!n.ok)throw new Error(`[tina4] rtc config fetch failed: ${n.status}`);return n.json()}function pt(){let e=window.location;return`${e.protocol==="https:"?"wss:":"ws:"}//${e.host}`}function Ae(e){return/^wss?:\/\//.test(e)?e:pt()+(e.startsWith("/")?e:"/"+e)}function mt(){let e=globalThis.crypto;if(e&&"randomUUID"in e)return e.randomUUID().slice(0,8);let n="";for(let t=0;t<8;t++)n+=Math.floor(16*(.5+t)).toString(16);return n+Date.now().toString(16).slice(-4)}async function ht(e,n={}){let t=k("connecting"),r=k(null),o=k([]),s=k(!1),i=k(null),l=mt(),d=n.config??await Z(n.configUrl),f=n.iceServers??d.iceServers??[],h=n.signallingUrl??d.signalling??"/ws/rtc",w=Ae(h.includes("{room}")?h.replace("{room}",e):`${h}/${e}`),u=null;n.media instanceof MediaStream?u=n.media:n.media!==!1&&(u=await navigator.mediaDevices.getUserMedia(n.media??{audio:!0,video:!0})),r.value=u;let a=u?.getVideoTracks()[0]??null,c=new Map,g=W.connect(w);function y(){o.value=[...c.entries()].map(([S,v])=>({id:S,stream:v.stream}))}function C(S){try{g.send({...S,from:l})}catch{}}function m(S){let v=c.get(S);if(v)return v;let p=new RTCPeerConnection({iceServers:f}),b={pc:p,polite:l<S,makingOffer:!1,ignoreOffer:!1,stream:null};if(c.set(S,b),u)for(let E of u.getTracks())p.addTrack(E,u);return p.onnegotiationneeded=async()=>{try{b.makingOffer=!0,await p.setLocalDescription(),C({type:"desc",to:S,description:p.localDescription})}catch(E){i.value=E}finally{b.makingOffer=!1}},p.onicecandidate=({candidate:E})=>{E&&C({type:"ice",to:S,candidate:E})},p.ontrack=({streams:E})=>{b.stream=E[0]??null,y()},p.onconnectionstatechange=()=>{["failed","closed"].includes(p.connectionState)?T(S):p.connectionState==="connected"&&(t.value="connected")},y(),b}function T(S){let v=c.get(S);if(v){try{v.pc.close()}catch{}c.delete(S),y()}}async function _(S){let v=S,p=v.from;if(!p||p===l||v.to&&v.to!==l)return;if(v.type==="hello"){m(p),C({type:"welcome",to:p});return}if(v.type==="welcome"){m(p);return}if(v.type==="bye"){T(p);return}let b=m(p),E=b.pc;if(v.type==="desc"){let M=v.description,N=M.type==="offer"&&(b.makingOffer||E.signalingState!=="stable");if(b.ignoreOffer=!b.polite&&N,b.ignoreOffer)return;await E.setRemoteDescription(M),M.type==="offer"&&(await E.setLocalDescription(),C({type:"desc",to:p,description:E.localDescription}))}else if(v.type==="ice")try{await E.addIceCandidate(v.candidate)}catch(M){b.ignoreOffer||(i.value=M)}}g.on("message",S=>{_(S)}),g.on("open",()=>{C({type:"hello"})});async function A(S){if(S)for(let{pc:v}of c.values()){let p=v.getSenders().find(b=>b.track?.kind==="video");p&&await p.replaceTrack(S)}}async function K(){await A(a),s.value=!1}async function Q(){let v=(await navigator.mediaDevices.getDisplayMedia({video:!0})).getVideoTracks()[0];await A(v),v.onended=()=>{K()},s.value=!0}return{status:t,localStream:r,peers:o,screenSharing:s,error:i,id:l,shareScreen:Q,stopScreen:K,toggleAudio(S){let v=u?.getAudioTracks()[0];return v?(v.enabled=S??!v.enabled,v.enabled):!1},toggleVideo(S){let v=u?.getVideoTracks()[0];return v?(v.enabled=S??!v.enabled,v.enabled):!1},leave(){C({type:"bye"});for(let S of[...c.keys()])T(S);if(u)for(let S of u.getTracks())S.stop();g.close(),t.value="closed"}}}function vt(e,n={}){let t=k([]),r=k([]),o=k([]),s=new Map,i=n.typingTimeout??3e3,l=n.url??"/ws/chat",d=Ae(l.includes("{channel}")?l.replace("{channel}",String(e)):`${l}/${e}`),f=W.connect(d,{token:n.token});function h(a){o.value.includes(a)||(o.value=[...o.value,a]);let c=s.get(a);c&&clearTimeout(c),s.set(a,setTimeout(()=>{o.value=o.value.filter(g=>g!==a),s.delete(a)},i))}f.on("message",a=>{let c=a;switch(c.type){case"message":t.value=[...t.value,c.message];break;case"presence":c.event==="roster"?r.value=c.users??[]:c.event==="join"&&c.user_id?r.value=[...new Set([...r.value,c.user_id])]:c.event==="leave"&&(r.value=r.value.filter(g=>g!==c.user_id));break;case"typing":c.user_id&&h(c.user_id);break}});let w=n.apiBase??"",u=n.messagesPath??"/api/channels/{id}/messages";return{status:f.status,connected:f.connected,messages:t,presence:r,typing:o,send(a,c){f.send({type:"message",body:a,thread_id:c??null})},sendTyping(){f.send({type:"typing"})},markRead(){f.send({type:"read"})},async history(a,c=50){let g=u.replace("{id}",String(e)),y=new URLSearchParams({limit:String(c)});a&&y.set("before",String(a));let C={};n.token&&(C.Authorization=`Bearer ${n.token}`);let m=await fetch(`${w}${g}?${y}`,{headers:C});if(!m.ok)throw new Error(`[tina4] chat history failed: ${m.status}`);let T=await m.json(),_=[...T].reverse();return t.value=[..._,...t.value],T},close(){for(let a of s.values())clearTimeout(a);s.clear(),f.close()}}}async function yt(e,n,t={}){let r=t.filesPath??"/api/files",o=new FormData;o.append("channel_id",String(e)),o.append("file",n,n.name??"file");let s={};t.token&&(s.Authorization=`Bearer ${t.token}`);let i=await fetch(`${t.apiBase??""}${r}`,{method:"POST",body:o,headers:s});if(!i.ok)throw new Error(`[tina4] file upload failed: ${i.status}`);return i.json()}async function bt(e,n={}){let t=/^https?:\/\//.test(e)?e:`${n.apiBase??""}${n.filesPath??"/api/files"}/${e}`,r={};n.token&&(r.Authorization=`Bearer ${n.token}`);let o=await fetch(t,{headers:r});if(!o.ok)throw new Error(`[tina4] file fetch failed: ${o.status}`);return URL.createObjectURL(await o.blob())}var Ie={config:Z,call:ht,chat:vt,upload:yt,fetchBlob:bt};var Pe={read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},Ne=/(token|password|passwd|secret|api[_-]?key|apikey|auth(?!or)|credential|jwt|bearer|otp|seed|private[_-]?key|session[_-]?id)/i,St=/^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}$/,kt=/^[A-Za-z0-9+/_=-]{40,}$/,Le=new Set;function De(e,n){if(Ne.test(e))return`key name "${e}" looks like a credential`;if(typeof n=="string"){if(St.test(n))return"value looks like a JWT";if(n.length>=40&&kt.test(n))return"value looks like a long base64 / token"}if(n&&typeof n=="object"&&!Array.isArray(n)){for(let t of Object.keys(n))if(Ne.test(t))return`object contains a credential-shape field "${t}"`}return null}function Fe(e,n){Le.has(n)||(Le.add(n),console.warn(`[tina4 persist] ${e} (key: ${JSON.stringify(n)}). localStorage is XSS-readable and never appropriate for credentials, tokens, passwords, personal data, or secrets. See STORAGE.md.`))}function qe(e){if(typeof globalThis>"u")return null;try{let n=e==="local"?globalThis.localStorage:globalThis.sessionStorage;return!n||typeof n.getItem!="function"?null:n}catch{return null}}function Ue(e,n){let{key:t,storage:r="local",serializer:o=Pe,version:s=1,migrate:i,syncTabs:l=!1,silenceCredentialWarning:d=!1}=n;if(!t||typeof t!="string")throw new Error("[tina4 persist] options.key is required and must be a string");let f=o===Pe,h=qe(r);if(!h)return $e(e,()=>{},()=>{});try{let a=h.getItem(t);if(a!==null){let c,g;try{let y=JSON.parse(a);y&&typeof y=="object"&&"value"in y?(c=y.v,g=y.value):g=y}catch{g=f?a:o.read(a)}if(c===s||c===void 0){let y=f?g:o.read(typeof g=="string"?g:JSON.stringify(g));e.value=y}else if(i)try{e.value=i(g,c)}catch(y){console.warn(`[tina4 persist] migrate() threw for key "${t}":`,y)}else console.warn(`[tina4 persist] stored version ${c} does not match current ${s} for key "${t}", and no migrate() was provided. Discarding the stored value.`)}}catch(a){console.warn(`[tina4 persist] failed to read key "${t}":`,a)}if(!d){let a=De(t,e.peek());a&&Fe(a,t)}let w=R(()=>{let a=e.value;if(!d){let c=De(t,a);c&&Fe(c,t)}try{let g=JSON.stringify(f?{v:s,value:a}:{v:s,value:o.write(a)});h.setItem(t,g)}catch(c){console.warn(`[tina4 persist] failed to write key "${t}":`,c)}}),u=null;if(l&&typeof globalThis<"u"&&"addEventListener"in globalThis){let a=c=>{let g=c;if(g.storageArea===h&&g.key===t&&g.newValue!==null)try{let y=JSON.parse(g.newValue),C=y&&typeof y=="object"&&"v"in y?y.v:void 0,m=C!==void 0?y.value:y;C!==void 0&&C!==s&&i?e.value=i(m,C):(C===s||C===void 0)&&(e.value=f?m:o.read(typeof m=="string"?m:JSON.stringify(m)))}catch(y){console.warn(`[tina4 persist] failed to parse storage event for key "${t}":`,y)}};globalThis.addEventListener?.("storage",a),u=()=>{globalThis.removeEventListener?.("storage",a)}}return $e(e,()=>{try{h.removeItem(t)}catch(a){console.warn(`[tina4 persist] failed to clear key "${t}":`,a)}},()=>{w(),u&&u()})}function $e(e,n,t){return Object.assign(e,{clear:n,dispose:t})}function je(e,n="local"){let t=qe(n);if(t)for(let r of e)try{t.removeItem(r)}catch(o){console.warn(`[tina4 persist] failed to clear key "${r}":`,o)}}var wt=["ar","he","fa","ur","ps","dv","syr","ckb","yi"];function Tt(){return globalThis.navigator?.language||"en"}function He(e,n="",t={}){for(let[r,o]of Object.entries(e)){let s=n?`${n}.${r}`:r;if(o!==null&&typeof o=="object"&&!Array.isArray(o))He(o,s,t);else{let i=String(o);t[s]=i,r in t||(t[r]=i)}}return t}function Ct(e,n){return e.replace(/\{(\w+)\}/g,(t,r)=>Object.prototype.hasOwnProperty.call(n,r)?String(n[r]):t)}function le(e={}){let n=e.locale||Tt(),t=e.fallbackLocale||n,r=new Set([...wt,...e.rtlLocales||[]]),o=k(n,"i18n.locale"),s=new Map,i=new Map;function l(u,a){let c=He(a),g=s.get(u);s.set(u,g?{...g,...c}:c)}if(e.messages)for(let[u,a]of Object.entries(e.messages))l(u,a);function d(u,a){return s.get(u)?.[a]}function f(u,a){let c=`n|${u}|${JSON.stringify(a||{})}`,g=i.get(c);return g||(g=new Intl.NumberFormat(u,a),i.set(c,g)),g}function h(u,a){let c=`d|${u}|${JSON.stringify(a||{})}`,g=i.get(c);return g||(g=new Intl.DateTimeFormat(u,a),i.set(c,g)),g}function w(u,a){let c=`r|${u}|${JSON.stringify(a||{})}`,g=i.get(c);return g||(g=new Intl.RelativeTimeFormat(u,a),i.set(c,g)),g}return{locale:o,t(u,a){let c=o.value,g=d(c,u);return g===void 0&&t!==c&&(g=d(t,u)),g===void 0&&(g=u),a?Ct(g,a):g},setLocale(u){o.value=u},getLocale(){return o.value},addMessages:l,hasLocale(u){return s.has(u)},availableLocales(){return[...s.keys()].sort()},async loadMessages(u,a){let c=await fetch(a);if(!c.ok)throw new Error(`[tina4 i18n] failed to load "${u}" from ${a}: ${c.status}`);l(u,await c.json())},number(u,a){return f(o.value,a).format(u)},currency(u,a,c){return f(o.value,{style:"currency",currency:a,...c}).format(u)},date(u,a){let c=u instanceof Date?u:new Date(u);return h(o.value,a).format(c)},relativeTime(u,a,c){return w(o.value,c||{numeric:"auto"}).format(u,a)},isRTL(){return r.has(o.value.split("-")[0].toLowerCase())},dir(){return this.isRTL()?"rtl":"ltr"}}}var We=le();return Xe(Et);})();
|
|
47
|
+
`.trim()}function Ie(e){let n={name:e.name,short_name:e.shortName??e.name,start_url:"/",display:e.display??"standalone",background_color:e.backgroundColor??"#ffffff",theme_color:e.themeColor??"#000000"};return e.icon&&(n.icons=[{src:e.icon,sizes:"192x192",type:"image/png"},{src:e.icon,sizes:"512x512",type:"image/png"}]),n}var Ne={register(e){let n=Ie(e),t=new Blob([JSON.stringify(n)],{type:"application/json"}),r=document.createElement("link");r.rel="manifest",r.href=URL.createObjectURL(t),document.head.appendChild(r);let o=document.querySelector('meta[name="theme-color"]');o||(o=document.createElement("meta"),o.name="theme-color",document.head.appendChild(o)),o.content=e.themeColor??"#000000","serviceWorker"in navigator&&(e.swUrl?navigator.serviceWorker.register(e.swUrl).catch(s=>{console.warn("[tina4] Service worker registration failed:",s)}):navigator.serviceWorker.register("/sw.js").catch(()=>{console.info("[tina4] No service worker at /sw.js. Use pwa.generateServiceWorker() to create one, or pass swUrl in config.")}))},generateServiceWorker(e){return mt(e)},generateManifest(e){return Ie(e)}};var ht={reconnect:!0,reconnectDelay:1e3,reconnectMaxDelay:3e4,reconnectAttempts:1/0,protocols:[],token:""};function vt(e){let n=Array.isArray(e.protocols)?e.protocols:e.protocols?[e.protocols]:[];return e.token?["bearer",e.token,...n]:e.protocols}function yt(e,n={}){let t={...ht,...n},r=w("connecting"),o=w(!1),s=w(null),i=w(null),c=w(0),a={message:[],open:[],close:[],error:[]},l=null,g=!1,m=t.reconnectDelay,u=null,d=0;function f(v){if(typeof v!="string")return v;try{return JSON.parse(v)}catch{return v}}function h(){r.value=d>0?"reconnecting":"connecting";try{l=new WebSocket(e,vt(t))}catch{r.value="closed",o.value=!1;return}l.onopen=()=>{r.value="open",o.value=!0,i.value=null,d=0,m=t.reconnectDelay,c.value=0;for(let v of a.open)v()},l.onmessage=v=>{let T=f(v.data);s.value=T;for(let R of a.message)R(T)},l.onclose=v=>{r.value="closed",o.value=!1;for(let T of a.close)T(v.code,v.reason);!g&&t.reconnect&&d<t.reconnectAttempts&&x()},l.onerror=v=>{i.value=v;for(let T of a.error)T(v)}}function x(){d++,c.value=d,r.value="reconnecting",u=setTimeout(()=>{u=null,h()},m),m=Math.min(m*2,t.reconnectMaxDelay)}let C={status:r,connected:o,lastMessage:s,error:i,reconnectCount:c,send(v){if(!l||l.readyState!==WebSocket.OPEN)throw new Error("[tina4] WebSocket is not connected");let T=typeof v=="string"?v:JSON.stringify(v);l.send(T)},on(v,T){return a[v].push(T),()=>{let R=a[v],A=R.indexOf(T);A>=0&&R.splice(A,1)}},pipe(v,T){let R=A=>{v.value=T(A,v.value)};return C.on("message",R)},close(v,T){g=!0,u&&(clearTimeout(u),u=null),l&&l.close(v??1e3,T??""),r.value="closed",o.value=!1}};return h(),C}var K={connect:yt};var St={mode:"eventsource",method:"GET",headers:{},body:void 0,reconnect:!0,reconnectDelay:1e3,reconnectMaxDelay:3e4,reconnectAttempts:1/0,events:[],json:!0};function bt(e,n={}){let t={...St,...n},r=w("connecting"),o=w(!1),s=w(null),i=w(null),c=w(null),a=w(0),l={message:[],open:[],close:[],error:[]},g=null,m=null,u=!1,d=t.reconnectDelay,f=null,h=0;function x(p){if(!t.json||typeof p!="string")return p;try{return JSON.parse(p)}catch{return p}}function C(p,S){s.value=p,i.value=S;for(let k of l.message)k(p,S??void 0)}function v(){r.value="open",o.value=!0,c.value=null,h=0,d=t.reconnectDelay,a.value=0;for(let p of l.open)p()}function T(){r.value="closed",o.value=!1;for(let p of l.close)p();!u&&t.reconnect&&h<t.reconnectAttempts&&Y()}function R(p){c.value=p;for(let S of l.error)S(p)}function A(){r.value=h>0?"reconnecting":"connecting";try{g=new EventSource(e)}catch{r.value="closed",o.value=!1;return}g.onopen=()=>v(),g.onmessage=p=>{C(x(p.data),null)};for(let p of t.events)g.addEventListener(p,S=>{C(x(S.data),p)});g.onerror=p=>{R(p),g&&g.readyState===2&&(g=null,T())}}function z(){r.value=h>0?"reconnecting":"connecting",m=new AbortController;let p={method:t.method,headers:t.headers,signal:m.signal};t.body!==void 0&&(p.body=typeof t.body=="string"?t.body:JSON.stringify(t.body)),fetch(e,p).then(async S=>{if(!S.ok){R(new Error(`[tina4] SSE fetch ${S.status}`)),T();return}v();let k=S.body.getReader(),M=new TextDecoder,P="";for(;;){let{done:Ge,value:Ze}=await k.read();if(Ge)break;P+=M.decode(Ze,{stream:!0});let ge=P.split(`
|
|
48
|
+
`);P=ge.pop();for(let Qe of ge){let pe=Qe.trim();pe&&C(x(pe),null)}}let fe=P.trim();fe&&C(x(fe),null),m=null,T()}).catch(S=>{S.name!=="AbortError"&&(m=null,R(S),T())})}function Y(){h++,a.value=h,r.value="reconnecting",f=setTimeout(()=>{f=null,b()},d),d=Math.min(d*2,t.reconnectMaxDelay)}function b(){t.mode==="fetch"?z():A()}let y={status:r,connected:o,lastMessage:s,lastEvent:i,error:c,reconnectCount:a,on(p,S){return l[p].push(S),()=>{let k=l[p],M=k.indexOf(S);M>=0&&k.splice(M,1)}},pipe(p,S){let k=M=>{p.value=S(M,p.value)};return y.on("message",k)},close(){u=!0,f&&(clearTimeout(f),f=null),g&&(g.close(),g=null),m&&(m.abort(),m=null),r.value="closed",o.value=!1}};return b(),y}var Pe={connect:bt};async function X(e="/api/rtc/config"){let n=await fetch(e);if(!n.ok)throw new Error(`[tina4] rtc config fetch failed: ${n.status}`);return n.json()}function wt(){let e=window.location;return`${e.protocol==="https:"?"wss:":"ws:"}//${e.host}`}function Le(e){return/^wss?:\/\//.test(e)?e:wt()+(e.startsWith("/")?e:"/"+e)}function Tt(){let e=globalThis.crypto;if(e&&"randomUUID"in e)return e.randomUUID().slice(0,8);let n="";for(let t=0;t<8;t++)n+=Math.floor(16*(.5+t)).toString(16);return n+Date.now().toString(16).slice(-4)}async function kt(e,n={}){let t=w("connecting"),r=w(null),o=w([]),s=w(!1),i=w(null),c=Tt(),a=n.config??await X(n.configUrl),l=n.iceServers??a.iceServers??[],g=n.signallingUrl??a.signalling??"/ws/rtc",m=Le(g.includes("{room}")?g.replace("{room}",e):`${g}/${e}`),u=null;n.media instanceof MediaStream?u=n.media:n.media!==!1&&(u=await navigator.mediaDevices.getUserMedia(n.media??{audio:!0,video:!0})),r.value=u;let d=u?.getVideoTracks()[0]??null,f=new Map,h=K.connect(m);function x(){o.value=[...f.entries()].map(([b,y])=>({id:b,stream:y.stream}))}function C(b){try{h.send({...b,from:c})}catch{}}function v(b){let y=f.get(b);if(y)return y;let p=new RTCPeerConnection({iceServers:l}),S={pc:p,polite:c<b,makingOffer:!1,ignoreOffer:!1,stream:null};if(f.set(b,S),u)for(let k of u.getTracks())p.addTrack(k,u);return p.onnegotiationneeded=async()=>{try{S.makingOffer=!0,await p.setLocalDescription(),C({type:"desc",to:b,description:p.localDescription})}catch(k){i.value=k}finally{S.makingOffer=!1}},p.onicecandidate=({candidate:k})=>{k&&C({type:"ice",to:b,candidate:k})},p.ontrack=({streams:k})=>{S.stream=k[0]??null,x()},p.onconnectionstatechange=()=>{["failed","closed"].includes(p.connectionState)?T(b):p.connectionState==="connected"&&(t.value="connected")},x(),S}function T(b){let y=f.get(b);if(y){try{y.pc.close()}catch{}f.delete(b),x()}}async function R(b){let y=b,p=y.from;if(!p||p===c||y.to&&y.to!==c)return;if(y.type==="hello"){v(p),C({type:"welcome",to:p});return}if(y.type==="welcome"){v(p);return}if(y.type==="bye"){T(p);return}let S=v(p),k=S.pc;if(y.type==="desc"){let M=y.description,P=M.type==="offer"&&(S.makingOffer||k.signalingState!=="stable");if(S.ignoreOffer=!S.polite&&P,S.ignoreOffer)return;await k.setRemoteDescription(M),M.type==="offer"&&(await k.setLocalDescription(),C({type:"desc",to:p,description:k.localDescription}))}else if(y.type==="ice")try{await k.addIceCandidate(y.candidate)}catch(M){S.ignoreOffer||(i.value=M)}}h.on("message",b=>{R(b)}),h.on("open",()=>{C({type:"hello"})});async function A(b){if(b)for(let{pc:y}of f.values()){let p=y.getSenders().find(S=>S.track?.kind==="video");p&&await p.replaceTrack(b)}}async function z(){await A(d),s.value=!1}async function Y(){let y=(await navigator.mediaDevices.getDisplayMedia({video:!0})).getVideoTracks()[0];await A(y),y.onended=()=>{z()},s.value=!0}return{status:t,localStream:r,peers:o,screenSharing:s,error:i,id:c,shareScreen:Y,stopScreen:z,toggleAudio(b){let y=u?.getAudioTracks()[0];return y?(y.enabled=b??!y.enabled,y.enabled):!1},toggleVideo(b){let y=u?.getVideoTracks()[0];return y?(y.enabled=b??!y.enabled,y.enabled):!1},leave(){C({type:"bye"});for(let b of[...f.keys()])T(b);if(u)for(let b of u.getTracks())b.stop();h.close(),t.value="closed"}}}function Ct(e,n={}){let t=w([]),r=w([]),o=w([]),s=new Map,i=n.typingTimeout??3e3,c=n.url??"/ws/chat",a=Le(c.includes("{channel}")?c.replace("{channel}",String(e)):`${c}/${e}`),l=K.connect(a,{token:n.token});function g(d){o.value.includes(d)||(o.value=[...o.value,d]);let f=s.get(d);f&&clearTimeout(f),s.set(d,setTimeout(()=>{o.value=o.value.filter(h=>h!==d),s.delete(d)},i))}l.on("message",d=>{let f=d;switch(f.type){case"message":t.value=[...t.value,f.message];break;case"presence":f.event==="roster"?r.value=f.users??[]:f.event==="join"&&f.user_id?r.value=[...new Set([...r.value,f.user_id])]:f.event==="leave"&&(r.value=r.value.filter(h=>h!==f.user_id));break;case"typing":f.user_id&&g(f.user_id);break}});let m=n.apiBase??"",u=n.messagesPath??"/api/channels/{id}/messages";return{status:l.status,connected:l.connected,messages:t,presence:r,typing:o,send(d,f){l.send({type:"message",body:d,thread_id:f??null})},sendTyping(){l.send({type:"typing"})},markRead(){l.send({type:"read"})},async history(d,f=50){let h=u.replace("{id}",String(e)),x=new URLSearchParams({limit:String(f)});d&&x.set("before",String(d));let C={};n.token&&(C.Authorization=`Bearer ${n.token}`);let v=await fetch(`${m}${h}?${x}`,{headers:C});if(!v.ok)throw new Error(`[tina4] chat history failed: ${v.status}`);let T=await v.json(),R=[...T].reverse();return t.value=[...R,...t.value],T},close(){for(let d of s.values())clearTimeout(d);s.clear(),l.close()}}}async function Et(e,n,t={}){let r=t.filesPath??"/api/files",o=new FormData;o.append("channel_id",String(e)),o.append("file",n,n.name??"file");let s={};t.token&&(s.Authorization=`Bearer ${t.token}`);let i=await fetch(`${t.apiBase??""}${r}`,{method:"POST",body:o,headers:s});if(!i.ok)throw new Error(`[tina4] file upload failed: ${i.status}`);return i.json()}async function Rt(e,n={}){let t=/^https?:\/\//.test(e)?e:`${n.apiBase??""}${n.filesPath??"/api/files"}/${e}`,r={};n.token&&(r.Authorization=`Bearer ${n.token}`);let o=await fetch(t,{headers:r});if(!o.ok)throw new Error(`[tina4] file fetch failed: ${o.status}`);return URL.createObjectURL(await o.blob())}var De={config:X,call:kt,chat:Ct,upload:Et,fetchBlob:Rt};var Fe={read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},$e=/(token|password|passwd|secret|api[_-]?key|apikey|auth(?!or)|credential|jwt|bearer|otp|seed|private[_-]?key|session[_-]?id)/i,_t=/^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}$/,xt=/^[A-Za-z0-9+/_=-]{40,}$/,qe=new Set;function Mt(e,n){if($e.test(e))return`key name "${e}" looks like a credential`;if(typeof n=="string"){if(_t.test(n))return"value looks like a JWT";if(n.length>=40&&xt.test(n))return"value looks like a long base64 / token"}if(n&&typeof n=="object"&&!Array.isArray(n)){for(let t of Object.keys(n))if($e.test(t))return`object contains a credential-shape field "${t}"`}return null}function Ot(e,n){qe.has(n)||(qe.add(n),console.warn(`[tina4 persist] ${e} (key: ${JSON.stringify(n)}). localStorage is XSS-readable and never appropriate for credentials, tokens, passwords, personal data, or secrets. See STORAGE.md.`))}function He(e){if(typeof globalThis>"u")return null;try{let n=e==="local"?globalThis.localStorage:globalThis.sessionStorage;return!n||typeof n.getItem!="function"?null:n}catch{return null}}function je(e,n,t){try{let r=JSON.parse(e);return r&&typeof r=="object"&&"value"in r?{version:r.v,payload:r.value}:{version:void 0,payload:r}}catch{return{version:void 0,payload:t?e:n.read(e)}}}function We(e,n,t){return t?e:n.read(typeof e=="string"?e:JSON.stringify(e))}function At(e,n,t,r,o,s,i){try{let c=n.getItem(t);if(c===null)return;let a=je(c,o,s);if(a.version===r||a.version===void 0){e.value=We(a.payload,o,s);return}if(i){try{e.value=i(a.payload,a.version)}catch(l){console.warn(`[tina4 persist] migrate() threw for key "${t}":`,l)}return}console.warn(`[tina4 persist] stored version ${a.version} does not match current ${r} for key "${t}", and no migrate() was provided. Discarding the stored value.`)}catch(c){console.warn(`[tina4 persist] failed to read key "${t}":`,c)}}function Ke(e,n,t){if(t)return;let r=Mt(e,n);r&&Ot(r,e)}function It(e,n,t,r,o,s,i){return E(()=>{let c=e.value;Ke(t,c,i);try{let a=JSON.stringify(s?{v:r,value:c}:{v:r,value:o.write(c)});n.setItem(t,a)}catch(a){console.warn(`[tina4 persist] failed to write key "${t}":`,a)}})}function Nt(e,n,t,r,o,s,i,c){if(!c||typeof globalThis>"u"||!("addEventListener"in globalThis))return null;let a=l=>{let g=l;if(!(g.storageArea!==n||g.key!==t||g.newValue===null))try{let m=je(g.newValue,o,s);m.version!==void 0&&m.version!==r&&i?e.value=i(m.payload,m.version):(m.version===r||m.version===void 0)&&(e.value=We(m.payload,o,s))}catch(m){console.warn(`[tina4 persist] failed to parse storage event for key "${t}":`,m)}};return globalThis.addEventListener?.("storage",a),()=>{globalThis.removeEventListener?.("storage",a)}}function Pt(e,n){try{e.removeItem(n)}catch(t){console.warn(`[tina4 persist] failed to clear key "${n}":`,t)}}function ze(e,n){let{key:t,storage:r="local",serializer:o=Fe,version:s=1,migrate:i,syncTabs:c=!1,silenceCredentialWarning:a=!1}=n;if(!t||typeof t!="string")throw new Error("[tina4 persist] options.key is required and must be a string");let l=o===Fe,g=He(r);if(!g)return Ue(e,()=>{},()=>{});At(e,g,t,s,o,l,i),Ke(t,e.peek(),a);let m=It(e,g,t,s,o,l,a),u=Nt(e,g,t,s,o,l,i,c);return Ue(e,()=>Pt(g,t),()=>{m(),u&&u()})}function Ue(e,n,t){return Object.assign(e,{clear:n,dispose:t})}function Ve(e,n="local"){let t=He(n);if(t)for(let r of e)try{t.removeItem(r)}catch(o){console.warn(`[tina4 persist] failed to clear key "${r}":`,o)}}var Lt=["ar","he","fa","ur","ps","dv","syr","ckb","yi"];function Dt(){return globalThis.navigator?.language||"en"}function Be(e,n="",t={}){for(let[r,o]of Object.entries(e)){let s=n?`${n}.${r}`:r;if(o!==null&&typeof o=="object"&&!Array.isArray(o))Be(o,s,t);else{let i=String(o);t[s]=i,r in t||(t[r]=i)}}return t}function Ft(e,n){return e.replace(/\{(\w+)\}/g,(t,r)=>Object.prototype.hasOwnProperty.call(n,r)?String(n[r]):t)}function de(e={}){let n=e.locale||Dt(),t=e.fallbackLocale||n,r=new Set([...Lt,...e.rtlLocales||[]]),o=w(n,"i18n.locale"),s=new Map,i=new Map;function c(u,d){let f=Be(d),h=s.get(u);s.set(u,h?{...h,...f}:f)}if(e.messages)for(let[u,d]of Object.entries(e.messages))c(u,d);function a(u,d){return s.get(u)?.[d]}function l(u,d){let f=`n|${u}|${JSON.stringify(d||{})}`,h=i.get(f);return h||(h=new Intl.NumberFormat(u,d),i.set(f,h)),h}function g(u,d){let f=`d|${u}|${JSON.stringify(d||{})}`,h=i.get(f);return h||(h=new Intl.DateTimeFormat(u,d),i.set(f,h)),h}function m(u,d){let f=`r|${u}|${JSON.stringify(d||{})}`,h=i.get(f);return h||(h=new Intl.RelativeTimeFormat(u,d),i.set(f,h)),h}return{locale:o,t(u,d){let f=o.value,h=a(f,u);return h===void 0&&t!==f&&(h=a(t,u)),h===void 0&&(h=u),d?Ft(h,d):h},setLocale(u){o.value=u},getLocale(){return o.value},addMessages:c,hasLocale(u){return s.has(u)},availableLocales(){return[...s.keys()].sort()},async loadMessages(u,d){let f=await fetch(d);if(!f.ok)throw new Error(`[tina4 i18n] failed to load "${u}" from ${d}: ${f.status}`);c(u,await f.json())},number(u,d){return l(o.value,d).format(u)},currency(u,d,f){return l(o.value,{style:"currency",currency:d,...f}).format(u)},date(u,d){let f=u instanceof Date?u:new Date(u);return g(o.value,d).format(f)},relativeTime(u,d,f){return m(o.value,f||{numeric:"auto"}).format(u,d)},isRTL(){return r.has(o.value.split("-")[0].toLowerCase())},dir(){return this.isRTL()?"rtl":"ltr"}}}var Je=de();return rt($t);})();
|
|
@@ -16,6 +16,9 @@ import type { Tina4Request, Tina4Response } from "./types.js";
|
|
|
16
16
|
export interface AuthGateRoute {
|
|
17
17
|
secure?: boolean;
|
|
18
18
|
noAuth?: boolean;
|
|
19
|
+
/** RBAC guard groups (Feature 138): OR within a group, AND across groups. */
|
|
20
|
+
requiredRoles?: string[][];
|
|
21
|
+
requiredPerms?: string[][];
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -68,7 +71,8 @@ export function enforceRouteAuth(
|
|
|
68
71
|
const identity = sso?.identity;
|
|
69
72
|
if (identity?.issuer && identity?.subject) {
|
|
70
73
|
req.user = identity;
|
|
71
|
-
|
|
74
|
+
// RBAC guards apply to the SSO identity too (Feature 138).
|
|
75
|
+
return rbacForbidden(match, identity, res);
|
|
72
76
|
}
|
|
73
77
|
const sessionToken = (req as any).session?.get?.("token") as string | undefined;
|
|
74
78
|
if (sessionToken && validToken(sessionToken)) {
|
|
@@ -93,5 +97,61 @@ export function enforceRouteAuth(
|
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
// ── RBAC guards (Feature 138): authorization AFTER authentication ──
|
|
101
|
+
// Auth has passed (401 ruled out above). If the route carries role/permission
|
|
102
|
+
// guards, the verified payload must satisfy them, else 403.
|
|
103
|
+
return rbacForbidden(match, req.user, res);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Read a claim as a list of strings; coerce a legacy singular string. */
|
|
107
|
+
function rbacClaimList(subject: Record<string, unknown>, key: string, legacy?: string): string[] {
|
|
108
|
+
const coerce = (v: unknown): string[] => {
|
|
109
|
+
if (typeof v === "string") return v === "" ? [] : [v];
|
|
110
|
+
if (Array.isArray(v)) return v.map((x) => String(x)).filter((x) => x !== "");
|
|
111
|
+
return [];
|
|
112
|
+
};
|
|
113
|
+
let out = coerce(subject[key]);
|
|
114
|
+
if (out.length === 0 && legacy) out = coerce(subject[legacy]);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* True if any GRANTED permission satisfies the concrete REQUIRED one.
|
|
120
|
+
* `*` grants everything; `posts.*` grants `posts.<...>` on the dot boundary.
|
|
121
|
+
*/
|
|
122
|
+
function rbacPermGranted(granted: string[], required: string): boolean {
|
|
123
|
+
return granted.some(
|
|
124
|
+
(g) => g === "*" || g === required || (g.endsWith(".*") && required.startsWith(g.slice(0, -1))),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Write a 403 and return `true` when a route's RBAC guards are not satisfied by
|
|
130
|
+
* the verified payload; return `false` (no write) when authorised or unguarded.
|
|
131
|
+
* AND across guard groups, OR within a group. Feature 138 / ADR-0058.
|
|
132
|
+
*/
|
|
133
|
+
function rbacForbidden(match: AuthGateRoute, payload: unknown, res: Tina4Response): boolean {
|
|
134
|
+
const requiredRoles = match.requiredRoles ?? [];
|
|
135
|
+
const requiredPerms = match.requiredPerms ?? [];
|
|
136
|
+
if (requiredRoles.length === 0 && requiredPerms.length === 0) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const subject =
|
|
140
|
+
payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
|
|
141
|
+
|
|
142
|
+
const roles = rbacClaimList(subject, "roles", "role");
|
|
143
|
+
for (const group of requiredRoles) {
|
|
144
|
+
if (!group.some((r) => roles.includes(r))) return writeForbidden(res);
|
|
145
|
+
}
|
|
146
|
+
const perms = rbacClaimList(subject, "permissions");
|
|
147
|
+
for (const group of requiredPerms) {
|
|
148
|
+
if (!group.some((p) => rbacPermGranted(perms, p))) return writeForbidden(res);
|
|
149
|
+
}
|
|
96
150
|
return false;
|
|
97
151
|
}
|
|
152
|
+
|
|
153
|
+
function writeForbidden(res: Tina4Response): boolean {
|
|
154
|
+
res.raw.writeHead(403, { "Content-Type": "application/json" });
|
|
155
|
+
res.raw.end(JSON.stringify({ error: "Forbidden" }));
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
@@ -69,6 +69,8 @@ interface MatchResult {
|
|
|
69
69
|
secure?: boolean;
|
|
70
70
|
cached?: boolean;
|
|
71
71
|
noAuth?: boolean;
|
|
72
|
+
requiredRoles?: string[][];
|
|
73
|
+
requiredPerms?: string[][];
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
interface CompiledRoute {
|
|
@@ -86,6 +88,9 @@ interface CompiledRoute {
|
|
|
86
88
|
cacheStore?: Map<string, { data: unknown; expires: number }>;
|
|
87
89
|
cacheTtl?: number;
|
|
88
90
|
template?: string;
|
|
91
|
+
/** RBAC guard groups (Feature 138): OR within a group, AND across groups. */
|
|
92
|
+
requiredRoles?: string[][];
|
|
93
|
+
requiredPerms?: string[][];
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
|
@@ -126,6 +131,33 @@ export class RouteRef {
|
|
|
126
131
|
return this;
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
/**
|
|
135
|
+
* RBAC: require ONE of the named roles (OR). Reads the verified JWT `roles`
|
|
136
|
+
* claim. Chain .role()/.can() for AND. Implies auth. Feature 138 / ADR-0058.
|
|
137
|
+
*/
|
|
138
|
+
role(...names: string[]): this {
|
|
139
|
+
const clean = names.filter((n) => n !== "");
|
|
140
|
+
if (clean.length > 0) {
|
|
141
|
+
(this.route.requiredRoles ??= []).push(clean);
|
|
142
|
+
this.route.secure = true;
|
|
143
|
+
}
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* RBAC: require ONE of the named permissions (OR). Reads the verified JWT
|
|
149
|
+
* `permissions` claim; granted-side wildcards (`posts.*`, `*`) satisfy a
|
|
150
|
+
* concrete requirement. Chain for AND. Implies auth. Feature 138.
|
|
151
|
+
*/
|
|
152
|
+
can(...permissions: string[]): this {
|
|
153
|
+
const clean = permissions.filter((p) => p !== "");
|
|
154
|
+
if (clean.length > 0) {
|
|
155
|
+
(this.route.requiredPerms ??= []).push(clean);
|
|
156
|
+
this.route.secure = true;
|
|
157
|
+
}
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
/** Mark this route's response as cacheable. */
|
|
130
162
|
cache(): this {
|
|
131
163
|
this.route.cached = true;
|
|
@@ -220,6 +252,8 @@ export class Router {
|
|
|
220
252
|
cached: definition.cached,
|
|
221
253
|
noAuth: definition.noAuth,
|
|
222
254
|
template: definition.template,
|
|
255
|
+
requiredRoles: definition.requiredRoles,
|
|
256
|
+
requiredPerms: definition.requiredPerms,
|
|
223
257
|
};
|
|
224
258
|
routes.push(compiled);
|
|
225
259
|
return new RouteRef(compiled);
|
|
@@ -393,6 +427,8 @@ export class Router {
|
|
|
393
427
|
secure: route.secure,
|
|
394
428
|
cached: route.cached,
|
|
395
429
|
noAuth: route.noAuth,
|
|
430
|
+
requiredRoles: route.requiredRoles,
|
|
431
|
+
requiredPerms: route.requiredPerms,
|
|
396
432
|
};
|
|
397
433
|
}
|
|
398
434
|
}
|
|
@@ -417,6 +453,8 @@ export class Router {
|
|
|
417
453
|
secure: route.secure,
|
|
418
454
|
cached: route.cached,
|
|
419
455
|
noAuth: route.noAuth,
|
|
456
|
+
requiredRoles: route.requiredRoles,
|
|
457
|
+
requiredPerms: route.requiredPerms,
|
|
420
458
|
});
|
|
421
459
|
}
|
|
422
460
|
}
|
|
@@ -1210,6 +1210,30 @@ function asHtmlString(chunk: unknown): string | null {
|
|
|
1210
1210
|
return null;
|
|
1211
1211
|
}
|
|
1212
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* Whether this response's body can still have HTML spliced into it.
|
|
1215
|
+
*
|
|
1216
|
+
* `text/html` is NOT enough on its own. A static-file response (static.ts) gzips
|
|
1217
|
+
* itself and sets Content-Encoding BEFORE it calls `res.raw.end()` - and that
|
|
1218
|
+
* `end()` is the intercepted one below, so the chunk arriving there is
|
|
1219
|
+
* COMPRESSED BYTES, not markup. Reading them back as UTF-8 to inject a toolbar
|
|
1220
|
+
* replaces every byte outside ASCII with U+FFFD, and the browser is handed a
|
|
1221
|
+
* gzip stream whose header is `1f ef bf bd` instead of `1f 8b`. Chrome answers
|
|
1222
|
+
* ERR_CONTENT_DECODING_FAILED and the page does not load at all.
|
|
1223
|
+
*
|
|
1224
|
+
* That is not a corner case: in dev mode it corrupted EVERY static .html file
|
|
1225
|
+
* over the 1024-byte compression threshold, which is most real pages, so
|
|
1226
|
+
* `tina4 serve` served an unloadable page while curl (which asks for no
|
|
1227
|
+
* encoding by default) looked perfectly healthy.
|
|
1228
|
+
*
|
|
1229
|
+
* dispatchPipeline.ts already guards the sibling half of this - it refuses to
|
|
1230
|
+
* gzip a body some earlier stage has already encoded - with the same test. This
|
|
1231
|
+
* is the other half: do not TEXT-EDIT a body some earlier stage has encoded.
|
|
1232
|
+
*/
|
|
1233
|
+
function isInjectableHtml(res: Tina4Response): boolean {
|
|
1234
|
+
return isHtmlResponse(res) && !res.raw.getHeader("content-encoding");
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1213
1237
|
/**
|
|
1214
1238
|
* Inject the dev toolbar (dev mode only) and the feedback widget into an HTML body.
|
|
1215
1239
|
*
|
|
@@ -1269,7 +1293,11 @@ function wrapResponseEnd(ctx: ResponseWrapContext): void {
|
|
|
1269
1293
|
);
|
|
1270
1294
|
}
|
|
1271
1295
|
|
|
1272
|
-
|
|
1296
|
+
// An ENCODED body is passed straight through, untouched and with its
|
|
1297
|
+
// Content-Length intact: the length static.ts set describes the compressed
|
|
1298
|
+
// bytes and is correct, and there is nothing here we could inject into
|
|
1299
|
+
// without destroying them. See isInjectableHtml.
|
|
1300
|
+
if (isInjectableHtml(res)) {
|
|
1273
1301
|
const html = asHtmlString(chunk);
|
|
1274
1302
|
if (html !== null) chunk = injectIntoHtml(ctx, devToolbar, html);
|
|
1275
1303
|
// Dropped for ANY html response, not only one carrying a body: that is
|
|
@@ -161,6 +161,10 @@ export interface RouteDefinition {
|
|
|
161
161
|
cached?: boolean;
|
|
162
162
|
/** Opt out of secure-by-default auth on write routes */
|
|
163
163
|
noAuth?: boolean;
|
|
164
|
+
/** RBAC role guard groups (Feature 138): OR within a group, AND across groups */
|
|
165
|
+
requiredRoles?: string[][];
|
|
166
|
+
/** RBAC permission guard groups (Feature 138) */
|
|
167
|
+
requiredPerms?: string[][];
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
export interface RouteMeta {
|
|
@@ -8337,6 +8337,31 @@ var init_router = __esm({
|
|
|
8337
8337
|
this.route.noAuth = true;
|
|
8338
8338
|
return this;
|
|
8339
8339
|
}
|
|
8340
|
+
/**
|
|
8341
|
+
* RBAC: require ONE of the named roles (OR). Reads the verified JWT `roles`
|
|
8342
|
+
* claim. Chain .role()/.can() for AND. Implies auth. Feature 138 / ADR-0058.
|
|
8343
|
+
*/
|
|
8344
|
+
role(...names) {
|
|
8345
|
+
const clean = names.filter((n) => n !== "");
|
|
8346
|
+
if (clean.length > 0) {
|
|
8347
|
+
(this.route.requiredRoles ??= []).push(clean);
|
|
8348
|
+
this.route.secure = true;
|
|
8349
|
+
}
|
|
8350
|
+
return this;
|
|
8351
|
+
}
|
|
8352
|
+
/**
|
|
8353
|
+
* RBAC: require ONE of the named permissions (OR). Reads the verified JWT
|
|
8354
|
+
* `permissions` claim; granted-side wildcards (`posts.*`, `*`) satisfy a
|
|
8355
|
+
* concrete requirement. Chain for AND. Implies auth. Feature 138.
|
|
8356
|
+
*/
|
|
8357
|
+
can(...permissions) {
|
|
8358
|
+
const clean = permissions.filter((p) => p !== "");
|
|
8359
|
+
if (clean.length > 0) {
|
|
8360
|
+
(this.route.requiredPerms ??= []).push(clean);
|
|
8361
|
+
this.route.secure = true;
|
|
8362
|
+
}
|
|
8363
|
+
return this;
|
|
8364
|
+
}
|
|
8340
8365
|
/** Mark this route's response as cacheable. */
|
|
8341
8366
|
cache() {
|
|
8342
8367
|
this.route.cached = true;
|
|
@@ -8404,7 +8429,9 @@ var init_router = __esm({
|
|
|
8404
8429
|
secure: secureDefault,
|
|
8405
8430
|
cached: definition.cached,
|
|
8406
8431
|
noAuth: definition.noAuth,
|
|
8407
|
-
template: definition.template
|
|
8432
|
+
template: definition.template,
|
|
8433
|
+
requiredRoles: definition.requiredRoles,
|
|
8434
|
+
requiredPerms: definition.requiredPerms
|
|
8408
8435
|
};
|
|
8409
8436
|
routes.push(compiled);
|
|
8410
8437
|
return new RouteRef(compiled);
|
|
@@ -8551,7 +8578,9 @@ var init_router = __esm({
|
|
|
8551
8578
|
template: route.template,
|
|
8552
8579
|
secure: route.secure,
|
|
8553
8580
|
cached: route.cached,
|
|
8554
|
-
noAuth: route.noAuth
|
|
8581
|
+
noAuth: route.noAuth,
|
|
8582
|
+
requiredRoles: route.requiredRoles,
|
|
8583
|
+
requiredPerms: route.requiredPerms
|
|
8555
8584
|
};
|
|
8556
8585
|
}
|
|
8557
8586
|
}
|
|
@@ -8574,7 +8603,9 @@ var init_router = __esm({
|
|
|
8574
8603
|
template: route.template,
|
|
8575
8604
|
secure: route.secure,
|
|
8576
8605
|
cached: route.cached,
|
|
8577
|
-
noAuth: route.noAuth
|
|
8606
|
+
noAuth: route.noAuth,
|
|
8607
|
+
requiredRoles: route.requiredRoles,
|
|
8608
|
+
requiredPerms: route.requiredPerms
|
|
8578
8609
|
});
|
|
8579
8610
|
}
|
|
8580
8611
|
}
|
|
@@ -8936,7 +8967,7 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
8936
8967
|
const identity = sso?.identity;
|
|
8937
8968
|
if (identity?.issuer && identity?.subject) {
|
|
8938
8969
|
req2.user = identity;
|
|
8939
|
-
return
|
|
8970
|
+
return rbacForbidden(match, identity, res);
|
|
8940
8971
|
}
|
|
8941
8972
|
const sessionToken = req2.session?.get?.("token");
|
|
8942
8973
|
if (sessionToken && validToken(sessionToken)) {
|
|
@@ -8956,8 +8987,45 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
8956
8987
|
res.header("FreshToken", fresh);
|
|
8957
8988
|
}
|
|
8958
8989
|
}
|
|
8990
|
+
return rbacForbidden(match, req2.user, res);
|
|
8991
|
+
}
|
|
8992
|
+
function rbacClaimList(subject, key, legacy) {
|
|
8993
|
+
const coerce = (v) => {
|
|
8994
|
+
if (typeof v === "string") return v === "" ? [] : [v];
|
|
8995
|
+
if (Array.isArray(v)) return v.map((x) => String(x)).filter((x) => x !== "");
|
|
8996
|
+
return [];
|
|
8997
|
+
};
|
|
8998
|
+
let out = coerce(subject[key]);
|
|
8999
|
+
if (out.length === 0 && legacy) out = coerce(subject[legacy]);
|
|
9000
|
+
return out;
|
|
9001
|
+
}
|
|
9002
|
+
function rbacPermGranted(granted, required) {
|
|
9003
|
+
return granted.some(
|
|
9004
|
+
(g) => g === "*" || g === required || g.endsWith(".*") && required.startsWith(g.slice(0, -1))
|
|
9005
|
+
);
|
|
9006
|
+
}
|
|
9007
|
+
function rbacForbidden(match, payload, res) {
|
|
9008
|
+
const requiredRoles = match.requiredRoles ?? [];
|
|
9009
|
+
const requiredPerms = match.requiredPerms ?? [];
|
|
9010
|
+
if (requiredRoles.length === 0 && requiredPerms.length === 0) {
|
|
9011
|
+
return false;
|
|
9012
|
+
}
|
|
9013
|
+
const subject = payload && typeof payload === "object" ? payload : {};
|
|
9014
|
+
const roles = rbacClaimList(subject, "roles", "role");
|
|
9015
|
+
for (const group of requiredRoles) {
|
|
9016
|
+
if (!group.some((r) => roles.includes(r))) return writeForbidden(res);
|
|
9017
|
+
}
|
|
9018
|
+
const perms = rbacClaimList(subject, "permissions");
|
|
9019
|
+
for (const group of requiredPerms) {
|
|
9020
|
+
if (!group.some((p) => rbacPermGranted(perms, p))) return writeForbidden(res);
|
|
9021
|
+
}
|
|
8959
9022
|
return false;
|
|
8960
9023
|
}
|
|
9024
|
+
function writeForbidden(res) {
|
|
9025
|
+
res.raw.writeHead(403, { "Content-Type": "application/json" });
|
|
9026
|
+
res.raw.end(JSON.stringify({ error: "Forbidden" }));
|
|
9027
|
+
return true;
|
|
9028
|
+
}
|
|
8961
9029
|
var init_authGate = __esm({
|
|
8962
9030
|
"../core/src/authGate.ts"() {
|
|
8963
9031
|
"use strict";
|
|
@@ -25034,6 +25102,9 @@ function asHtmlString(chunk) {
|
|
|
25034
25102
|
if (Buffer.isBuffer(chunk)) return chunk.toString("utf-8");
|
|
25035
25103
|
return null;
|
|
25036
25104
|
}
|
|
25105
|
+
function isInjectableHtml(res) {
|
|
25106
|
+
return isHtmlResponse(res) && !res.raw.getHeader("content-encoding");
|
|
25107
|
+
}
|
|
25037
25108
|
function injectIntoHtml(ctx, devToolbar, html) {
|
|
25038
25109
|
if (!devToolbar) return injectFeedbackWidget(ctx.req, html);
|
|
25039
25110
|
const toolbarCtx = {
|
|
@@ -25061,7 +25132,7 @@ function wrapResponseEnd(ctx) {
|
|
|
25061
25132
|
Date.now() - ctx.reqStartTime
|
|
25062
25133
|
);
|
|
25063
25134
|
}
|
|
25064
|
-
if (
|
|
25135
|
+
if (isInjectableHtml(res)) {
|
|
25065
25136
|
const html = asHtmlString(chunk);
|
|
25066
25137
|
if (html !== null) chunk = injectIntoHtml(ctx, devToolbar, html);
|
|
25067
25138
|
if (!res.raw.headersSent) res.raw.removeHeader("content-length");
|
|
@@ -3,6 +3,9 @@ import type { Tina4Request, Tina4Response } from "./types.js";
|
|
|
3
3
|
export interface AuthGateRoute {
|
|
4
4
|
secure?: boolean;
|
|
5
5
|
noAuth?: boolean;
|
|
6
|
+
/** RBAC guard groups (Feature 138): OR within a group, AND across groups. */
|
|
7
|
+
requiredRoles?: string[][];
|
|
8
|
+
requiredPerms?: string[][];
|
|
6
9
|
}
|
|
7
10
|
/**
|
|
8
11
|
* Enforce auth for a matched route.
|
|
@@ -17,6 +17,8 @@ interface MatchResult {
|
|
|
17
17
|
secure?: boolean;
|
|
18
18
|
cached?: boolean;
|
|
19
19
|
noAuth?: boolean;
|
|
20
|
+
requiredRoles?: string[][];
|
|
21
|
+
requiredPerms?: string[][];
|
|
20
22
|
}
|
|
21
23
|
interface CompiledRoute {
|
|
22
24
|
pattern: string;
|
|
@@ -36,6 +38,9 @@ interface CompiledRoute {
|
|
|
36
38
|
}>;
|
|
37
39
|
cacheTtl?: number;
|
|
38
40
|
template?: string;
|
|
41
|
+
/** RBAC guard groups (Feature 138): OR within a group, AND across groups. */
|
|
42
|
+
requiredRoles?: string[][];
|
|
43
|
+
requiredPerms?: string[][];
|
|
39
44
|
}
|
|
40
45
|
/**
|
|
41
46
|
* Thin reference to a registered WebSocket route, enabling chained modifiers
|
|
@@ -63,6 +68,17 @@ export declare class RouteRef {
|
|
|
63
68
|
secure(): this;
|
|
64
69
|
/** Opt out of secure-by-default auth (for public write routes). */
|
|
65
70
|
noAuth(): this;
|
|
71
|
+
/**
|
|
72
|
+
* RBAC: require ONE of the named roles (OR). Reads the verified JWT `roles`
|
|
73
|
+
* claim. Chain .role()/.can() for AND. Implies auth. Feature 138 / ADR-0058.
|
|
74
|
+
*/
|
|
75
|
+
role(...names: string[]): this;
|
|
76
|
+
/**
|
|
77
|
+
* RBAC: require ONE of the named permissions (OR). Reads the verified JWT
|
|
78
|
+
* `permissions` claim; granted-side wildcards (`posts.*`, `*`) satisfy a
|
|
79
|
+
* concrete requirement. Chain for AND. Implies auth. Feature 138.
|
|
80
|
+
*/
|
|
81
|
+
can(...permissions: string[]): this;
|
|
66
82
|
/** Mark this route's response as cacheable. */
|
|
67
83
|
cache(): this;
|
|
68
84
|
/**
|
|
@@ -146,6 +146,10 @@ export interface RouteDefinition {
|
|
|
146
146
|
cached?: boolean;
|
|
147
147
|
/** Opt out of secure-by-default auth on write routes */
|
|
148
148
|
noAuth?: boolean;
|
|
149
|
+
/** RBAC role guard groups (Feature 138): OR within a group, AND across groups */
|
|
150
|
+
requiredRoles?: string[][];
|
|
151
|
+
/** RBAC permission guard groups (Feature 138) */
|
|
152
|
+
requiredPerms?: string[][];
|
|
149
153
|
}
|
|
150
154
|
export interface RouteMeta {
|
|
151
155
|
summary?: string;
|