tina4ruby 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/README.md +2 -3
- data/lib/tina4/database_adapter.rb +46 -14
- data/lib/tina4/public/js/tina4js.min.js +3 -3
- data/lib/tina4/rack_app.rb +57 -0
- data/lib/tina4/router.rb +30 -0
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c940eabf57b7ba299db01f414b5d24d949af99cf970d49fa677e69f77b9192c9
|
|
4
|
+
data.tar.gz: 83fb893d4db991b8393f22e727d3b7b9e220553f9118eb8a8a52a176dbea390b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a04a51dbdd80fb05f7213c3983a03e5bf351eebf8ad5ef197e8e80c6489e47ace86a0417131cc2d3a8bbca6554c42dc96016f66c098cc103a5c4f411980137f
|
|
7
|
+
data.tar.gz: f4e952bb42149b90c62b4bba12695c02234fdf11c19f30bb0744031190144bc0b02c13c7d55916ac1bc9c18f130e66a504e071383ac221413a3b7980e2d1f5ac
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ number means the same thing everywhere.
|
|
|
6
6
|
**The authoritative release notes for every shipped version live in the documentation:**
|
|
7
7
|
https://tina4.com/ruby/36-releases
|
|
8
8
|
|
|
9
|
+
## 3.13.107
|
|
10
|
+
|
|
11
|
+
Feature: RBAC role and permission guards (parity across all four frameworks).
|
|
12
|
+
`role(...)` / `can(...)` route guards read the cryptographically-verified JWT
|
|
13
|
+
`roles` / `permissions` claims — OR within a guard, AND by stacking guards, with
|
|
14
|
+
granted-side wildcards (`posts.*`, `*`) and a legacy singular `role` claim coerced
|
|
15
|
+
to a list. A guarded route implies auth: no or invalid token → 401, authenticated
|
|
16
|
+
but unauthorised → 403; the handler never runs on a miss. Feature 138 / ADR-0058.
|
|
17
|
+
|
|
9
18
|
## 3.13.105
|
|
10
19
|
|
|
11
20
|
Bug release. Route inspection stops touching the app; Firebird's migration
|
|
@@ -26,6 +35,65 @@ filename that broke Windows checkouts.
|
|
|
26
35
|
Firebird driver returns for the `migration_name` column.
|
|
27
36
|
- Uses the atomic sequence table pattern already in place for other engines.
|
|
28
37
|
|
|
38
|
+
### Queue and ORM audit bugs closed
|
|
39
|
+
|
|
40
|
+
- **`Model.clear_cache()` cascades to `db.cache_clear()`.** Under both
|
|
41
|
+
`TINA4_AUTO_CACHING=true` and `TINA4_DB_CACHE=true`, a manual `clear_cache()`
|
|
42
|
+
used to leave the DB-layer cache holding stale rows (PY-06-22).
|
|
43
|
+
- **`Queue.retry()` revives every dead letter.** The no-arg form used a
|
|
44
|
+
generator inside `any(...)` and short-circuited after the first success.
|
|
45
|
+
Now materialised so all N dead letters revive (PY-12-04).
|
|
46
|
+
- **`Job.retry()` unlinks the dead-letter file.** Iterating `dead_letters()`
|
|
47
|
+
and calling `.retry()` on each used to leave the failed directory carrying
|
|
48
|
+
every revived job, so the next `dead_letters()` call re-reported them
|
|
49
|
+
(PY-12-05).
|
|
50
|
+
- **MongoDB `retry_job(id)` searches the dead-letter namespace by data.id,
|
|
51
|
+
deletes the DL doc, and upserts the original back to pending.** The old
|
|
52
|
+
filter `{_id, self._topic, "failed"}` could never match a dead letter.
|
|
53
|
+
- **MongoDB `purge(status)` returns `deleted_count`, honours every status, and
|
|
54
|
+
routes dead/failed/dead_letter aliases to the `.dead_letter` namespace.**
|
|
55
|
+
Previously returned None and only handled `pending` — via `clear()`, which
|
|
56
|
+
nuked every doc under the topic regardless of status.
|
|
57
|
+
|
|
58
|
+
### Test-harness fixes
|
|
59
|
+
|
|
60
|
+
- **SIGTERM port probe checks both interfaces.** The graceful-shutdown test
|
|
61
|
+
probed only `0.0.0.0`; on macOS with the framework's default `127.0.0.1`
|
|
62
|
+
listener holding the port, that bind succeeded and the pre-assertion
|
|
63
|
+
concluded the port was free. Now probes both interfaces.
|
|
64
|
+
- **MySQL connect-timeout assertion parses the reported elapsed via regex**
|
|
65
|
+
and checks it against the configured bound instead of the outer clock;
|
|
66
|
+
mysql-connector's post-abort cleanup adds ~0.7s that the two stopwatches
|
|
67
|
+
legitimately disagreed on.
|
|
68
|
+
- **AI installer tests sandbox `HOME` per-test.** `install_context()` writes
|
|
69
|
+
the global skills bundle to `Path.home()/.claude/skills` by design; in the
|
|
70
|
+
test suite that raced other tests and hit stale root-owned files from prior
|
|
71
|
+
sudo runs. `monkeypatch.setenv("HOME", ...)` redirects the global install
|
|
72
|
+
to a throwaway dir.
|
|
73
|
+
|
|
74
|
+
### Developer skills — announce and 💩
|
|
75
|
+
|
|
76
|
+
- **Announce before you act.** All four framework developer skills now
|
|
77
|
+
instruct: say what you are about to do in one line before doing it —
|
|
78
|
+
Plan / Next / Done. Never write more than two files between announcements.
|
|
79
|
+
Never run a schema migration, install a dependency, or edit the boot file
|
|
80
|
+
without a preceding "About to:" line.
|
|
81
|
+
- **💩 stale-skill detection.** All four framework developer skills gained
|
|
82
|
+
`updated_for_version: 3.13.105` in the frontmatter and a self-check that
|
|
83
|
+
compares this to the latest published skill version at
|
|
84
|
+
`https://tina4.com/skills/<name>/version` (never against the project's
|
|
85
|
+
framework version — the framework version is the developer's call). If a
|
|
86
|
+
newer skill is available, 💩 rides beside 🤖 on every reply plus a one-time
|
|
87
|
+
update instruction.
|
|
88
|
+
|
|
89
|
+
### Doc parity
|
|
90
|
+
|
|
91
|
+
- `Queue.size()`, `Queue.failed()` and `Queue.dead_letters()` gained matching
|
|
92
|
+
in-source docstrings across Python, PHP, Ruby and Node: `size("failed")` /
|
|
93
|
+
`size("dead")` / `size("dead_letter")` are aliases for the dead-letter
|
|
94
|
+
count (== length of `dead_letters()`); `failed()` lists retryable-but-
|
|
95
|
+
attempted jobs, counted under `size("pending")`, NOT `size("failed")`.
|
|
96
|
+
|
|
29
97
|
## 3.13.103
|
|
30
98
|
|
|
31
99
|
### Metrics reports what it can prove
|
data/README.md
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
<h1 align="center">Tina4 Ruby</h1>
|
|
5
5
|
<h3 align="center">TINA4: The Intelligent Native Application 4ramework</h3>
|
|
6
6
|
<p align="center"><em>Simple. Fast. Human. | Built for AI. Built for you.</em></p>
|
|
7
|
-
<p align="center">
|
|
7
|
+
<p align="center">Zero runtime dependencies. One require, everything works.</p>
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://rubygems.org/gems/tina4ruby"><img src="https://img.shields.io/gem/v/tina4ruby?color=7b1fa2&label=RubyGems" alt="RubyGems"></a>
|
|
10
|
-
<img src="https://img.shields.io/
|
|
11
|
-
<img src="https://img.shields.io/badge/features-98-blue" alt="Features">
|
|
10
|
+
<a href="https://github.com/tina4stack/tina4-ruby/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/tina4stack/tina4-ruby/test.yml?label=tests" alt="Tests"></a>
|
|
12
11
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
13
12
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
14
13
|
</p>
|
|
@@ -262,12 +262,6 @@ module Tina4
|
|
|
262
262
|
CONNECT_TIMEOUT_VAR = "TINA4_DATABASE_CONNECT_TIMEOUT"
|
|
263
263
|
DEFAULT_CONNECT_TIMEOUT_SECONDS = 10
|
|
264
264
|
|
|
265
|
-
# Clock slack when deciding whether a failed connect was OUR bound expiring.
|
|
266
|
-
# A native bound of 10s is measured back as 9.998s often enough to matter,
|
|
267
|
-
# and without the slack the contract error would degrade into the raw driver
|
|
268
|
-
# error at random.
|
|
269
|
-
CONNECT_TIMEOUT_SLACK_SECONDS = 0.25
|
|
270
|
-
|
|
271
265
|
# Seconds to bound a connect by, or nil when the operator disabled the bound.
|
|
272
266
|
def self.connect_timeout_seconds
|
|
273
267
|
seconds = Tina4::Env.float(CONNECT_TIMEOUT_VAR, default: DEFAULT_CONNECT_TIMEOUT_SECONDS)
|
|
@@ -275,12 +269,20 @@ module Tina4
|
|
|
275
269
|
end
|
|
276
270
|
|
|
277
271
|
# Whole seconds for the native options that accept only an integer (libpq,
|
|
278
|
-
# libmysqlclient, FreeTDS).
|
|
279
|
-
#
|
|
280
|
-
#
|
|
272
|
+
# libmysqlclient, FreeTDS). STRICTLY greater than the bound, never below 1.
|
|
273
|
+
#
|
|
274
|
+
# floor(s) + 1, not ceil(s): the native option must land AFTER our bound so
|
|
275
|
+
# the driver's own timer fires first and bounding_connect gets to translate
|
|
276
|
+
# its failure. ceil(N) == N for a whole-second bound - and the shipped
|
|
277
|
+
# default of 10 is whole - which put the driver's deadline ON our bound
|
|
278
|
+
# instead of after it, so which message reached the caller came down to which
|
|
279
|
+
# clock ticked first. floor(s) + 1 is strictly greater for every input,
|
|
280
|
+
# whole or fractional, at a cost of at most one extra second on a path that
|
|
281
|
+
# has already failed. (libpq also reads connect_timeout=0 as "wait forever",
|
|
282
|
+
# so the +1 doubles as the guard against rounding a sub-second bound to 0.)
|
|
281
283
|
def self.connect_timeout_whole_seconds
|
|
282
284
|
seconds = connect_timeout_seconds
|
|
283
|
-
seconds && [seconds.
|
|
285
|
+
seconds && [seconds.floor + 1, 1].max
|
|
284
286
|
end
|
|
285
287
|
|
|
286
288
|
# The one error a timed-out connect raises: it names the host, the port, the
|
|
@@ -294,6 +296,27 @@ module Tina4
|
|
|
294
296
|
"indefinitely).#{detail}"
|
|
295
297
|
end
|
|
296
298
|
|
|
299
|
+
# Did a connect that FAILED take at least the configured bound?
|
|
300
|
+
#
|
|
301
|
+
# Two readings, because the framework and the driver do not share a clock.
|
|
302
|
+
# bounding_connect times on CLOCK_MONOTONIC; libpq times its own
|
|
303
|
+
# connect_timeout on gettimeofday - CLOCK_REALTIME (libmysqlclient and
|
|
304
|
+
# FreeTDS likewise measure against the wall clock). NTP slews and steps
|
|
305
|
+
# realtime and never touches monotonic, so a forward step or slew can make
|
|
306
|
+
# the driver abort while a monotonic reading over the very same connect is
|
|
307
|
+
# still short of the bound - and then the driver's own message, which names
|
|
308
|
+
# no tunable, reaches the caller.
|
|
309
|
+
#
|
|
310
|
+
# Taking the LARGER of the two readings covers both directions: the realtime
|
|
311
|
+
# reading catches a forward jump, and keeping the monotonic reading means a
|
|
312
|
+
# BACKWARD jump cannot hide a timeout that really did happen. Pure, so the
|
|
313
|
+
# decision is testable without faking a clock.
|
|
314
|
+
def self.bound_reached?(elapsed_monotonic, elapsed_realtime, seconds)
|
|
315
|
+
return false if seconds.nil?
|
|
316
|
+
|
|
317
|
+
[elapsed_monotonic, elapsed_realtime].max >= seconds
|
|
318
|
+
end
|
|
319
|
+
|
|
297
320
|
# Run a driver's natively-bounded connect and translate an expiry into the
|
|
298
321
|
# contract error above. The NATIVE option does the bounding; this only names
|
|
299
322
|
# it. Whether the bound expired is decided by ELAPSED TIME, not by matching
|
|
@@ -301,17 +324,26 @@ module Tina4
|
|
|
301
324
|
# ("timeout expired", "waiting for initial communication packet", "TDS
|
|
302
325
|
# server connection timed out", "Connection timed out"), and a marker table
|
|
303
326
|
# is one more thing to drift and MISS. A missed timeout is the whole defect.
|
|
327
|
+
#
|
|
328
|
+
# The elapsed time is read on BOTH clocks and compared through
|
|
329
|
+
# bound_reached?, because the driver measured its own deadline on the wall
|
|
330
|
+
# clock while we started ours on the monotonic one - see that method. The
|
|
331
|
+
# strictly-greater native option (connect_timeout_whole_seconds) leaves the
|
|
332
|
+
# driver's deadline after ours, so on an undisturbed clock the larger reading
|
|
333
|
+
# comfortably reaches the bound with no slack to tune.
|
|
304
334
|
def self.bounding_connect(host, port)
|
|
305
|
-
|
|
335
|
+
started_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
336
|
+
started_realtime = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
306
337
|
yield
|
|
307
338
|
rescue StandardError => error
|
|
308
339
|
bound = connect_timeout_seconds
|
|
309
340
|
raise if bound.nil?
|
|
310
341
|
|
|
311
|
-
|
|
312
|
-
|
|
342
|
+
elapsed_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_monotonic
|
|
343
|
+
elapsed_realtime = Process.clock_gettime(Process::CLOCK_REALTIME) - started_realtime
|
|
344
|
+
raise unless bound_reached?(elapsed_monotonic, elapsed_realtime, bound)
|
|
313
345
|
|
|
314
|
-
connect_timed_out!(host, port,
|
|
346
|
+
connect_timed_out!(host, port, [elapsed_monotonic, elapsed_realtime].max, error)
|
|
315
347
|
end
|
|
316
348
|
|
|
317
349
|
# Did this driver actually OVERRIDE the contract method, or is it inheriting
|
|
@@ -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);})();
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -1178,6 +1178,9 @@ module Tina4
|
|
|
1178
1178
|
identity = sso.is_a?(Hash) ? sso["identity"] : nil
|
|
1179
1179
|
if identity.is_a?(Hash) && identity["issuer"] && identity["subject"]
|
|
1180
1180
|
env["tina4.auth_payload"] = identity
|
|
1181
|
+
deny = rbac_forbidden(route, identity)
|
|
1182
|
+
return deny if deny
|
|
1183
|
+
|
|
1181
1184
|
return nil
|
|
1182
1185
|
end
|
|
1183
1186
|
session_token = session.get("token")
|
|
@@ -1207,9 +1210,63 @@ module Tina4
|
|
|
1207
1210
|
return [401, { "content-type" => "application/json" }, [JSON.generate({ error: "Unauthorized" })]]
|
|
1208
1211
|
end
|
|
1209
1212
|
|
|
1213
|
+
# ── RBAC guards (Feature 138): authorization AFTER authentication ──
|
|
1214
|
+
# Auth has passed (401 ruled out above). If the route carries role/
|
|
1215
|
+
# permission guards, the verified payload must satisfy them, else 403.
|
|
1216
|
+
deny = rbac_forbidden(route, env["tina4.auth_payload"])
|
|
1217
|
+
return deny if deny
|
|
1218
|
+
|
|
1210
1219
|
nil
|
|
1211
1220
|
end
|
|
1212
1221
|
|
|
1222
|
+
# Build a 403 Rack tuple when a route's RBAC guards are not satisfied by the
|
|
1223
|
+
# verified payload, else nil. AND across guard groups, OR within a group.
|
|
1224
|
+
# Roles read the `roles` claim (legacy singular `role` coerced); permissions
|
|
1225
|
+
# read `permissions` with granted-side wildcards. Feature 138 / ADR-0058.
|
|
1226
|
+
def self.rbac_forbidden(route, payload)
|
|
1227
|
+
required_roles = route.respond_to?(:roles) ? Array(route.roles) : []
|
|
1228
|
+
required_perms = route.respond_to?(:perms) ? Array(route.perms) : []
|
|
1229
|
+
return nil if required_roles.empty? && required_perms.empty?
|
|
1230
|
+
|
|
1231
|
+
subject = payload.is_a?(Hash) ? payload : {}
|
|
1232
|
+
roles = _rbac_claim_list(subject, "roles", "role")
|
|
1233
|
+
required_roles.each do |group|
|
|
1234
|
+
return _rbac_forbidden_tuple unless group.any? { |r| roles.include?(r) }
|
|
1235
|
+
end
|
|
1236
|
+
perms = _rbac_claim_list(subject, "permissions", nil)
|
|
1237
|
+
required_perms.each do |group|
|
|
1238
|
+
return _rbac_forbidden_tuple unless group.any? { |req| _rbac_perm_granted(perms, req) }
|
|
1239
|
+
end
|
|
1240
|
+
nil
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
def self._rbac_forbidden_tuple
|
|
1244
|
+
[403, { "content-type" => "application/json" }, [JSON.generate({ error: "Forbidden" })]]
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
# Read a claim as a list of strings; coerce a legacy singular string.
|
|
1248
|
+
def self._rbac_claim_list(subject, key, legacy)
|
|
1249
|
+
out = _rbac_coerce_list(subject[key])
|
|
1250
|
+
out = _rbac_coerce_list(subject[legacy]) if out.empty? && legacy
|
|
1251
|
+
out
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
def self._rbac_coerce_list(val)
|
|
1255
|
+
case val
|
|
1256
|
+
when String then val.empty? ? [] : [val]
|
|
1257
|
+
when Array then val.map(&:to_s).reject(&:empty?)
|
|
1258
|
+
else []
|
|
1259
|
+
end
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
# True if any GRANTED permission satisfies the concrete REQUIRED one.
|
|
1263
|
+
# `*` grants everything; `posts.*` grants `posts.<...>` on the dot boundary.
|
|
1264
|
+
def self._rbac_perm_granted(granted, required)
|
|
1265
|
+
granted.any? do |g|
|
|
1266
|
+
g == "*" || g == required || (g.end_with?(".*") && required.start_with?(g[0..-2]))
|
|
1267
|
+
end
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1213
1270
|
def self._read_rack_body(env)
|
|
1214
1271
|
# Route through the capped reader so the running per-chunk upload cap is
|
|
1215
1272
|
# enforced on the form-token / body path too: an over-limit body raises
|
data/lib/tina4/router.rb
CHANGED
|
@@ -8,6 +8,9 @@ module Tina4
|
|
|
8
8
|
attr_reader :method, :path, :handler, :auth_handler, :swagger_meta,
|
|
9
9
|
:path_regex, :param_names, :template
|
|
10
10
|
attr_accessor :auth_required, :cached
|
|
11
|
+
# RBAC guard groups (Feature 138 / ADR-0058). Each is an array of arrays:
|
|
12
|
+
# OR within a group, AND across groups (stacking).
|
|
13
|
+
attr_reader :roles, :perms
|
|
11
14
|
|
|
12
15
|
def initialize(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
|
|
13
16
|
@method = method.to_s.upcase.freeze
|
|
@@ -23,6 +26,8 @@ module Tina4
|
|
|
23
26
|
# PY-10-02. Call .no_auth on the route to opt out explicitly.
|
|
24
27
|
@auth_required = %w[POST PUT PATCH DELETE].include?(@method)
|
|
25
28
|
@cached = false
|
|
29
|
+
@roles = []
|
|
30
|
+
@perms = []
|
|
26
31
|
@param_names = []
|
|
27
32
|
@path_regex = compile_pattern(@path)
|
|
28
33
|
@param_names.freeze
|
|
@@ -42,6 +47,31 @@ module Tina4
|
|
|
42
47
|
self
|
|
43
48
|
end
|
|
44
49
|
|
|
50
|
+
# RBAC: require ONE of the named roles (OR). Reads the verified JWT `roles`
|
|
51
|
+
# claim. Stack .role/.can for AND. Implies auth. Feature 138 / ADR-0058.
|
|
52
|
+
# Returns self for chaining: Router.get("/admin") { ... }.role("admin")
|
|
53
|
+
def role(*names)
|
|
54
|
+
names = names.flatten.map(&:to_s).reject(&:empty?)
|
|
55
|
+
unless names.empty?
|
|
56
|
+
@roles << names
|
|
57
|
+
@auth_required = true
|
|
58
|
+
end
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# RBAC: require ONE of the named permissions (OR). Reads the verified JWT
|
|
63
|
+
# `permissions` claim; granted-side wildcards (`posts.*`, `*`) satisfy a
|
|
64
|
+
# concrete requirement. Stack for AND. Implies auth. Feature 138.
|
|
65
|
+
# Returns self for chaining: Router.delete("/p") { ... }.can("posts.delete")
|
|
66
|
+
def can(*permissions)
|
|
67
|
+
permissions = permissions.flatten.map(&:to_s).reject(&:empty?)
|
|
68
|
+
unless permissions.empty?
|
|
69
|
+
@perms << permissions
|
|
70
|
+
@auth_required = true
|
|
71
|
+
end
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
45
75
|
# Mark this route as cacheable.
|
|
46
76
|
# Returns self for chaining: Router.get("/path") { ... }.cache
|
|
47
77
|
def cache
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.108
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|