intigin 1.0.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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]
intigin/__main__.py ADDED
@@ -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,6 @@
1
+ intigin/__init__.py,sha256=QMzQraT3TTejNOdbeQyvcW7wu0g6UKDaJIrgyTEjDzA,7243
2
+ intigin/__main__.py,sha256=7NG1HgPKOCoZIGIh1FOiJJyA4gREP6XATRTo152fTX0,1693
3
+ intigin-1.0.0.dist-info/METADATA,sha256=we9BHeiM7KaMZYc02tESKa0AwZqyAOsV5VTgQh8pet4,3228
4
+ intigin-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
5
+ intigin-1.0.0.dist-info/top_level.txt,sha256=bywfZ-FI_37csye_pdEUeRjU622UP21ncPhLWJlVrrU,8
6
+ intigin-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ intigin