intigin 1.0.0__tar.gz
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.
- intigin-1.0.0/PKG-INFO +73 -0
- intigin-1.0.0/README.md +55 -0
- intigin-1.0.0/intigin/__init__.py +176 -0
- intigin-1.0.0/intigin/__main__.py +41 -0
- intigin-1.0.0/intigin.egg-info/PKG-INFO +73 -0
- intigin-1.0.0/intigin.egg-info/SOURCES.txt +8 -0
- intigin-1.0.0/intigin.egg-info/dependency_links.txt +1 -0
- intigin-1.0.0/intigin.egg-info/top_level.txt +1 -0
- intigin-1.0.0/pyproject.toml +28 -0
- intigin-1.0.0/setup.cfg +4 -0
intigin-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: intigin
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Intigin dynamic-backend adapter — thin in-app WSGI middleware for view-source protection.
|
|
5
|
+
Author: Intigin
|
|
6
|
+
Project-URL: Homepage, https://intigin.com
|
|
7
|
+
Project-URL: Documentation, https://intigin.com/docs
|
|
8
|
+
Keywords: intigin,wsgi,middleware,view-source,protection,flask,django
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: Other/Proprietary License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.7
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# intigin
|
|
20
|
+
|
|
21
|
+
Thin in-app **WSGI adapter** for [Intigin](https://intigin.com) dynamic-backend protection. It wraps
|
|
22
|
+
your app so the same Intigin bundle that protects static sites also protects your server-rendered pages —
|
|
23
|
+
view-source shows only a loader shell, and the real HTML is reconstructed client-side.
|
|
24
|
+
|
|
25
|
+
The adapter does almost nothing: the browser bundle does the heavy lifting. On a page navigation it
|
|
26
|
+
returns a **clean loader shell** (no payload, no cookie); when the bundle asks for the page (a same-URL
|
|
27
|
+
fetch carrying an `X-Intigin-Payload` header) it encodes the finished HTML into the frozen wire format
|
|
28
|
+
and returns it. The bundle at `https://{host}/{token}.js` decodes and reconstructs the page, then
|
|
29
|
+
reveals, polls, and hot-swaps live like a static Intigin page — no backend round-trip on updates.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install intigin
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Use
|
|
38
|
+
|
|
39
|
+
Wrap your WSGI callable with a scoped Gateway API key (dashboard → **Settings → Gateway API**). The
|
|
40
|
+
adapter resolves your public bundle token from the key once at boot, so no token is hardcoded:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from intigin import Intigin
|
|
44
|
+
|
|
45
|
+
app.wsgi_app = Intigin(app.wsgi_app, api_key="iga_...", host="intigin.com") # recommended
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Flask uses `app.wsgi_app`; Django wraps `application` in `wsgi.py`. A raw 32-hex `token="..."` is also
|
|
49
|
+
accepted for a zero-config demo.
|
|
50
|
+
|
|
51
|
+
## Behavior
|
|
52
|
+
|
|
53
|
+
- **Cookieless.** The payload rides the bundle's marker fetch (the browser's own session), so nothing
|
|
54
|
+
payload-related appears in the source and no cookie is set — cookie blockers don't affect it.
|
|
55
|
+
- **Non-HTML passes through** untouched; only top-level HTML navigations are wrapped.
|
|
56
|
+
- **Fail-closed.** A bad/missing key or any wrap error disables the adapter (serves your app
|
|
57
|
+
unprotected, logged to stderr) or returns a self-contained restricted page — never the raw HTML for a
|
|
58
|
+
failed encode.
|
|
59
|
+
- **Kill-switch.** `INTIGIN_ADAPTER_MODE=wrap|passthrough|off` (env). `passthrough`/`off` removes the
|
|
60
|
+
adapter from the path.
|
|
61
|
+
|
|
62
|
+
## Conformance
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python -m intigin --selftest
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Verifies the frozen wire format — `base64("IGP1" + FLAGS + BODY)`, `BODY = xor(deflate(utf8(html)),
|
|
69
|
+
sha256(token))` — against the browser bundle's decoder.
|
|
70
|
+
|
|
71
|
+
> **Concealment, not encryption.** The scramble key is derived from the public bundle token, so this
|
|
72
|
+
> defeats view-source and naive scrapers, not cryptanalysis. Object-level authorization stays in your
|
|
73
|
+
> own routes.
|
intigin-1.0.0/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# intigin
|
|
2
|
+
|
|
3
|
+
Thin in-app **WSGI adapter** for [Intigin](https://intigin.com) dynamic-backend protection. It wraps
|
|
4
|
+
your app so the same Intigin bundle that protects static sites also protects your server-rendered pages —
|
|
5
|
+
view-source shows only a loader shell, and the real HTML is reconstructed client-side.
|
|
6
|
+
|
|
7
|
+
The adapter does almost nothing: the browser bundle does the heavy lifting. On a page navigation it
|
|
8
|
+
returns a **clean loader shell** (no payload, no cookie); when the bundle asks for the page (a same-URL
|
|
9
|
+
fetch carrying an `X-Intigin-Payload` header) it encodes the finished HTML into the frozen wire format
|
|
10
|
+
and returns it. The bundle at `https://{host}/{token}.js` decodes and reconstructs the page, then
|
|
11
|
+
reveals, polls, and hot-swaps live like a static Intigin page — no backend round-trip on updates.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install intigin
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
Wrap your WSGI callable with a scoped Gateway API key (dashboard → **Settings → Gateway API**). The
|
|
22
|
+
adapter resolves your public bundle token from the key once at boot, so no token is hardcoded:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from intigin import Intigin
|
|
26
|
+
|
|
27
|
+
app.wsgi_app = Intigin(app.wsgi_app, api_key="iga_...", host="intigin.com") # recommended
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Flask uses `app.wsgi_app`; Django wraps `application` in `wsgi.py`. A raw 32-hex `token="..."` is also
|
|
31
|
+
accepted for a zero-config demo.
|
|
32
|
+
|
|
33
|
+
## Behavior
|
|
34
|
+
|
|
35
|
+
- **Cookieless.** The payload rides the bundle's marker fetch (the browser's own session), so nothing
|
|
36
|
+
payload-related appears in the source and no cookie is set — cookie blockers don't affect it.
|
|
37
|
+
- **Non-HTML passes through** untouched; only top-level HTML navigations are wrapped.
|
|
38
|
+
- **Fail-closed.** A bad/missing key or any wrap error disables the adapter (serves your app
|
|
39
|
+
unprotected, logged to stderr) or returns a self-contained restricted page — never the raw HTML for a
|
|
40
|
+
failed encode.
|
|
41
|
+
- **Kill-switch.** `INTIGIN_ADAPTER_MODE=wrap|passthrough|off` (env). `passthrough`/`off` removes the
|
|
42
|
+
adapter from the path.
|
|
43
|
+
|
|
44
|
+
## Conformance
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python -m intigin --selftest
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Verifies the frozen wire format — `base64("IGP1" + FLAGS + BODY)`, `BODY = xor(deflate(utf8(html)),
|
|
51
|
+
sha256(token))` — against the browser bundle's decoder.
|
|
52
|
+
|
|
53
|
+
> **Concealment, not encryption.** The scramble key is derived from the public bundle token, so this
|
|
54
|
+
> defeats view-source and naive scrapers, not cryptanalysis. Object-level authorization stays in your
|
|
55
|
+
> own routes.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Intigin WSGI adapter — dynamic backend protection (in-app, runs lite).
|
|
3
|
+
|
|
4
|
+
Thin intercept-and-wrap. The browser bundle does all the heavy lifting; the adapter only:
|
|
5
|
+
- returns a CLEAN loader shell for page navigations (view-source is just the loader — no payload,
|
|
6
|
+
no cookie, nothing to leak), and
|
|
7
|
+
- hands the bundle the encoded page when it asks for it: the bundle re-fetches the SAME url with an
|
|
8
|
+
`X-Intigin-Payload` header; the response carries the browser's own session (per-user correct),
|
|
9
|
+
cookieless, and never appears in the source.
|
|
10
|
+
|
|
11
|
+
The bundle at https://{host}/{token}.js decodes the wire and reconstructs the page, then reveals +
|
|
12
|
+
polls + hot-swaps live exactly like a static Intigin page — no backend round-trip on updates.
|
|
13
|
+
|
|
14
|
+
Wire: base64("IGP1" + FLAGS + BODY), BODY = xor(deflate(utf8(html)), sha256(token)),
|
|
15
|
+
compress-before-scramble, FLAGS = 0x01 DEFLATE | 0x02 SCRAMBLE.
|
|
16
|
+
|
|
17
|
+
Install:
|
|
18
|
+
pip install intigin
|
|
19
|
+
|
|
20
|
+
from intigin import Intigin
|
|
21
|
+
app.wsgi_app = Intigin(app.wsgi_app, api_key="iga_...", host="intigin.com") # recommended
|
|
22
|
+
app.wsgi_app = Intigin(app.wsgi_app, token="...32hex...", host="intigin.com") # zero-config demo
|
|
23
|
+
|
|
24
|
+
Fail-closed. Non-HTML passes through. Kill-switch: INTIGIN_ADAPTER_MODE = wrap | passthrough | off.
|
|
25
|
+
"""
|
|
26
|
+
import hashlib
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import urllib.request
|
|
30
|
+
import zlib
|
|
31
|
+
from base64 import b64encode
|
|
32
|
+
|
|
33
|
+
__version__ = "1.0.0"
|
|
34
|
+
__all__ = ["Intigin", "encode_payload", "shell", "RESTRICTED_HTML"]
|
|
35
|
+
|
|
36
|
+
MAGIC = b"IGP1"
|
|
37
|
+
FLAG_DEFLATE = 0x01
|
|
38
|
+
FLAG_SCRAMBLE = 0x02
|
|
39
|
+
PAYLOAD_HEADER_ENV = "HTTP_X_INTIGIN_PAYLOAD"
|
|
40
|
+
ASSET_EXT = (".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".woff",
|
|
41
|
+
".woff2", ".ttf", ".map", ".json", ".xml", ".txt", ".pdf", ".mp4", ".webm", ".wasm")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _key(token):
|
|
45
|
+
return hashlib.sha256(token.encode("ascii")).digest()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _xor(data, key):
|
|
49
|
+
klen = len(key)
|
|
50
|
+
return bytes(b ^ key[i % klen] for i, b in enumerate(data))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def encode_payload(html, token, compress=True):
|
|
54
|
+
body = html.encode("utf-8") if isinstance(html, str) else html
|
|
55
|
+
flags = 0
|
|
56
|
+
if compress:
|
|
57
|
+
body = zlib.compress(body, 9)
|
|
58
|
+
flags |= FLAG_DEFLATE
|
|
59
|
+
body = _xor(body, _key(token))
|
|
60
|
+
flags |= FLAG_SCRAMBLE
|
|
61
|
+
return b64encode(MAGIC + bytes([flags]) + body).decode("ascii")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def shell(host, token):
|
|
65
|
+
"""The clean loader — the ENTIRE view-source of a protected page."""
|
|
66
|
+
return (
|
|
67
|
+
"<!DOCTYPE html>\n"
|
|
68
|
+
'<html lang="en">\n'
|
|
69
|
+
"<!-- BEGIN Intigin -->\n"
|
|
70
|
+
'<script src="https://{host}/{token}.js" defer></script>\n'
|
|
71
|
+
'<noscript><meta http-equiv="refresh" content="0;url=about:blank"></noscript>\n'
|
|
72
|
+
"<!-- END Intigin -->\n"
|
|
73
|
+
"</html>"
|
|
74
|
+
).format(host=host, token=token).encode("utf-8")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
RESTRICTED_HTML = (
|
|
78
|
+
'<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Protected</title></head>'
|
|
79
|
+
'<body style="margin:0;background:#0a0e14;color:#7dd3fc;font:600 15px/1.5 system-ui,sans-serif;'
|
|
80
|
+
'display:flex;align-items:center;justify-content:center;height:100vh">'
|
|
81
|
+
"<div>Protected content is temporarily unavailable.</div></body></html>"
|
|
82
|
+
).encode("utf-8")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get(headers, name):
|
|
86
|
+
nl = name.lower()
|
|
87
|
+
return next((v for (k, v) in headers if k.lower() == nl), None)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _mode():
|
|
91
|
+
m = os.environ.get("INTIGIN_ADAPTER_MODE", "wrap").lower().strip()
|
|
92
|
+
return m if m in ("wrap", "passthrough", "off") else "wrap"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _resolve_token(api_key, host):
|
|
96
|
+
"""Resolve the embed's PUBLIC script token from a scoped iga_ key (chained-scoped-key auth)."""
|
|
97
|
+
req = urllib.request.Request("https://%s/api/v1/account/state" % host,
|
|
98
|
+
headers={"Authorization": "Bearer " + api_key})
|
|
99
|
+
with urllib.request.urlopen(req, timeout=10) as r:
|
|
100
|
+
tok = (json.load(r).get("script_token") or "").strip().lower()
|
|
101
|
+
if len(tok) != 32:
|
|
102
|
+
raise ValueError("account/state returned no script_token")
|
|
103
|
+
return "".join(c for c in tok if c in "0123456789abcdef")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Intigin:
|
|
107
|
+
def __init__(self, app, token=None, api_key=None, host="intigin.com"):
|
|
108
|
+
self.app = app
|
|
109
|
+
self.host = host.rstrip("/")
|
|
110
|
+
self.ok = True
|
|
111
|
+
try:
|
|
112
|
+
if api_key:
|
|
113
|
+
self.token = _resolve_token(api_key, self.host)
|
|
114
|
+
elif token:
|
|
115
|
+
self.token = "".join(c for c in token.lower() if c in "0123456789abcdef")
|
|
116
|
+
if len(self.token) != 32:
|
|
117
|
+
raise ValueError("token must be 32 hex chars")
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError("provide api_key='iga_...' (recommended) or token='...32hex...'")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
import sys
|
|
122
|
+
sys.stderr.write("[Intigin] adapter disabled: %s\n" % e)
|
|
123
|
+
self.token, self.ok = None, False
|
|
124
|
+
|
|
125
|
+
def _invoke(self, environ):
|
|
126
|
+
cap, chunks = {}, []
|
|
127
|
+
|
|
128
|
+
def start(status, headers, exc_info=None):
|
|
129
|
+
cap["status"], cap["headers"] = status, headers
|
|
130
|
+
return chunks.append
|
|
131
|
+
|
|
132
|
+
it = self.app(environ, start)
|
|
133
|
+
try:
|
|
134
|
+
for c in it:
|
|
135
|
+
if c:
|
|
136
|
+
chunks.append(c)
|
|
137
|
+
finally:
|
|
138
|
+
if hasattr(it, "close"):
|
|
139
|
+
it.close()
|
|
140
|
+
return b"".join(chunks), cap.get("status", "200 OK"), cap.get("headers", [])
|
|
141
|
+
|
|
142
|
+
def __call__(self, environ, start_response):
|
|
143
|
+
if not self.ok or _mode() == "off":
|
|
144
|
+
return self.app(environ, start_response)
|
|
145
|
+
method = environ.get("REQUEST_METHOD", "GET")
|
|
146
|
+
path = environ.get("PATH_INFO", "/")
|
|
147
|
+
accept = environ.get("HTTP_ACCEPT", "")
|
|
148
|
+
|
|
149
|
+
# 1) The bundle asking for the encoded page.
|
|
150
|
+
if environ.get(PAYLOAD_HEADER_ENV) and _mode() == "wrap":
|
|
151
|
+
try:
|
|
152
|
+
body, status, headers = self._invoke(environ)
|
|
153
|
+
if "text/html" in (_get(headers, "Content-Type") or "").lower() and body:
|
|
154
|
+
wire = encode_payload(body.decode("utf-8", "replace"), self.token).encode("ascii")
|
|
155
|
+
start_response("200 OK", [("Content-Type", "application/octet-stream"),
|
|
156
|
+
("Content-Length", str(len(wire))), ("Cache-Control", "no-store")])
|
|
157
|
+
return [wire]
|
|
158
|
+
start_response(status, headers) # non-HTML -> raw (bundle recovers)
|
|
159
|
+
return [body]
|
|
160
|
+
except Exception:
|
|
161
|
+
start_response("200 OK", [("Content-Type", "text/html; charset=utf-8"),
|
|
162
|
+
("Content-Length", str(len(RESTRICTED_HTML))), ("Cache-Control", "no-store")])
|
|
163
|
+
return [RESTRICTED_HTML]
|
|
164
|
+
|
|
165
|
+
# 2) Page navigation -> clean shell, no render.
|
|
166
|
+
if (_mode() == "wrap" and method == "GET" and "text/html" in accept.lower()
|
|
167
|
+
and not path.lower().endswith(ASSET_EXT)):
|
|
168
|
+
s = shell(self.host, self.token)
|
|
169
|
+
start_response("200 OK", [("Content-Type", "text/html; charset=utf-8"),
|
|
170
|
+
("Content-Length", str(len(s))), ("Cache-Control", "no-store")])
|
|
171
|
+
return [s]
|
|
172
|
+
|
|
173
|
+
# 3) Everything else (assets, POST, non-HTML) -> passthrough.
|
|
174
|
+
body, status, headers = self._invoke(environ)
|
|
175
|
+
start_response(status, headers)
|
|
176
|
+
return [body]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Conformance self-test: `python -m intigin --selftest` (also runs with no args).
|
|
2
|
+
|
|
3
|
+
Verifies the frozen golden vector and a compressed round-trip against the wire format the browser
|
|
4
|
+
bundle decodes, and that the loader shell carries no payload. Exits non-zero on any mismatch.
|
|
5
|
+
"""
|
|
6
|
+
import base64
|
|
7
|
+
import sys
|
|
8
|
+
import zlib
|
|
9
|
+
|
|
10
|
+
from intigin import MAGIC, encode_payload, shell, _key, _xor
|
|
11
|
+
|
|
12
|
+
GOLDEN_TOKEN = "0123456789abcdef0123456789abcdef"
|
|
13
|
+
GOLDEN_HTML = "<!DOCTYPE html><html><head><title>Golden</title></head><body><h1>Vector</h1></body></html>"
|
|
14
|
+
GOLDEN_B64 = ("SUdQMQICkPkM2hOyJmy4jRKhrt6lr+V85xF8P/mlk+QXJAjDlVuP+iz1I44Y"
|
|
15
|
+
"FbeRD7iuhaf7vnnuTiRpoKaYvlJuXd/IAOfYIO0omUoG8NRY8O2C9qPoL7cAKCPxqMk=")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
ok = True
|
|
20
|
+
|
|
21
|
+
good = encode_payload(GOLDEN_HTML, GOLDEN_TOKEN, compress=False) == GOLDEN_B64
|
|
22
|
+
ok = ok and good
|
|
23
|
+
print(("PASS" if good else "FAIL") + " golden vector (uncompressed frame is byte-stable)")
|
|
24
|
+
|
|
25
|
+
frame = base64.b64decode(encode_payload(GOLDEN_HTML, GOLDEN_TOKEN, compress=True))
|
|
26
|
+
rt = frame[:4] == MAGIC and zlib.decompress(_xor(frame[5:], _key(GOLDEN_TOKEN))).decode("utf-8") == GOLDEN_HTML
|
|
27
|
+
ok = ok and rt
|
|
28
|
+
print(("PASS" if rt else "FAIL") + " compressed round-trip decodes to the original HTML")
|
|
29
|
+
|
|
30
|
+
src = shell("intigin.com", GOLDEN_TOKEN)
|
|
31
|
+
clean = b"data-ig-payload" not in src and b"IGP1" not in src
|
|
32
|
+
ok = ok and clean
|
|
33
|
+
print(("PASS" if clean else "FAIL") + " clean loader shell carries no payload")
|
|
34
|
+
|
|
35
|
+
print("\n" + ("ALL PASS" if ok else "FAILURES"))
|
|
36
|
+
return 0 if ok else 1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
# `--selftest` is accepted for symmetry with the docs; the module self-tests unconditionally.
|
|
41
|
+
sys.exit(main())
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: intigin
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Intigin dynamic-backend adapter — thin in-app WSGI middleware for view-source protection.
|
|
5
|
+
Author: Intigin
|
|
6
|
+
Project-URL: Homepage, https://intigin.com
|
|
7
|
+
Project-URL: Documentation, https://intigin.com/docs
|
|
8
|
+
Keywords: intigin,wsgi,middleware,view-source,protection,flask,django
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: Other/Proprietary License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.7
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# intigin
|
|
20
|
+
|
|
21
|
+
Thin in-app **WSGI adapter** for [Intigin](https://intigin.com) dynamic-backend protection. It wraps
|
|
22
|
+
your app so the same Intigin bundle that protects static sites also protects your server-rendered pages —
|
|
23
|
+
view-source shows only a loader shell, and the real HTML is reconstructed client-side.
|
|
24
|
+
|
|
25
|
+
The adapter does almost nothing: the browser bundle does the heavy lifting. On a page navigation it
|
|
26
|
+
returns a **clean loader shell** (no payload, no cookie); when the bundle asks for the page (a same-URL
|
|
27
|
+
fetch carrying an `X-Intigin-Payload` header) it encodes the finished HTML into the frozen wire format
|
|
28
|
+
and returns it. The bundle at `https://{host}/{token}.js` decodes and reconstructs the page, then
|
|
29
|
+
reveals, polls, and hot-swaps live like a static Intigin page — no backend round-trip on updates.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install intigin
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Use
|
|
38
|
+
|
|
39
|
+
Wrap your WSGI callable with a scoped Gateway API key (dashboard → **Settings → Gateway API**). The
|
|
40
|
+
adapter resolves your public bundle token from the key once at boot, so no token is hardcoded:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from intigin import Intigin
|
|
44
|
+
|
|
45
|
+
app.wsgi_app = Intigin(app.wsgi_app, api_key="iga_...", host="intigin.com") # recommended
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Flask uses `app.wsgi_app`; Django wraps `application` in `wsgi.py`. A raw 32-hex `token="..."` is also
|
|
49
|
+
accepted for a zero-config demo.
|
|
50
|
+
|
|
51
|
+
## Behavior
|
|
52
|
+
|
|
53
|
+
- **Cookieless.** The payload rides the bundle's marker fetch (the browser's own session), so nothing
|
|
54
|
+
payload-related appears in the source and no cookie is set — cookie blockers don't affect it.
|
|
55
|
+
- **Non-HTML passes through** untouched; only top-level HTML navigations are wrapped.
|
|
56
|
+
- **Fail-closed.** A bad/missing key or any wrap error disables the adapter (serves your app
|
|
57
|
+
unprotected, logged to stderr) or returns a self-contained restricted page — never the raw HTML for a
|
|
58
|
+
failed encode.
|
|
59
|
+
- **Kill-switch.** `INTIGIN_ADAPTER_MODE=wrap|passthrough|off` (env). `passthrough`/`off` removes the
|
|
60
|
+
adapter from the path.
|
|
61
|
+
|
|
62
|
+
## Conformance
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python -m intigin --selftest
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Verifies the frozen wire format — `base64("IGP1" + FLAGS + BODY)`, `BODY = xor(deflate(utf8(html)),
|
|
69
|
+
sha256(token))` — against the browser bundle's decoder.
|
|
70
|
+
|
|
71
|
+
> **Concealment, not encryption.** The scramble key is derived from the public bundle token, so this
|
|
72
|
+
> defeats view-source and naive scrapers, not cryptanalysis. Object-level authorization stays in your
|
|
73
|
+
> own routes.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
intigin
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "intigin"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Intigin dynamic-backend adapter — thin in-app WSGI middleware for view-source protection."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
authors = [{ name = "Intigin" }]
|
|
12
|
+
keywords = ["intigin", "wsgi", "middleware", "view-source", "protection", "flask", "django"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: Other/Proprietary License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware",
|
|
20
|
+
"Topic :: Security",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://intigin.com"
|
|
25
|
+
Documentation = "https://intigin.com/docs"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
packages = ["intigin"]
|
intigin-1.0.0/setup.cfg
ADDED