jac-scale 0.1.3__py3-none-any.whl → 0.1.4__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.
@@ -3,6 +3,9 @@
3
3
  import contextlib
4
4
  import gc
5
5
  import glob
6
+ import hashlib
7
+ import hmac
8
+ import json
6
9
  import socket
7
10
  import subprocess
8
11
  import time
@@ -260,7 +263,7 @@ class TestJacScaleServe:
260
263
  def _create_expired_token(self, username: str, days_ago: int = 1) -> str:
261
264
  """Create an expired JWT token for testing."""
262
265
  # Use the same secret as the server (default)
263
- secret = "supersecretkey"
266
+ secret = "supersecretkey_for_testing_only!"
264
267
  algorithm = "HS256"
265
268
 
266
269
  past_time = datetime.now(UTC) - timedelta(days=days_ago)
@@ -273,7 +276,7 @@ class TestJacScaleServe:
273
276
 
274
277
  def _create_very_old_token(self, username: str, days_ago: int = 15) -> str:
275
278
  """Create a token that's too old to refresh."""
276
- secret = "supersecretkey"
279
+ secret = "supersecretkey_for_testing_only!"
277
280
  algorithm = "HS256"
278
281
 
279
282
  past_time = datetime.now(UTC) - timedelta(days=days_ago)
@@ -505,7 +508,7 @@ class TestJacScaleServe:
505
508
  new_token = refresh_result["token"]
506
509
 
507
510
  # Decode both tokens and verify username is preserved
508
- secret = "supersecretkey"
511
+ secret = "supersecretkey_for_testing_only!"
509
512
  algorithm = "HS256"
510
513
 
511
514
  original_payload = pyjwt.decode(original_token, secret, algorithms=[algorithm])
@@ -535,7 +538,7 @@ class TestJacScaleServe:
535
538
  new_token = refresh_result["token"]
536
539
 
537
540
  # Decode tokens and compare expiration times
538
- secret = "supersecretkey"
541
+ secret = "supersecretkey_for_testing_only!"
539
542
  algorithm = "HS256"
540
543
 
541
544
  original_payload = pyjwt.decode(original_token, secret, algorithms=[algorithm])
@@ -1561,6 +1564,356 @@ class TestJacScaleServe:
1561
1564
  )
1562
1565
  assert old_password_response.status_code == 401
1563
1566
 
1567
+ # ========================================================================
1568
+ # Webhook Walker Tests
1569
+ # ========================================================================
1570
+
1571
+ def _generate_webhook_signature(self, payload: bytes, secret: str) -> str:
1572
+ """Generate HMAC-SHA256 signature for webhook payload."""
1573
+ return hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
1574
+
1575
+ def test_webhook_endpoint_exists_for_webhook_walkers(self) -> None:
1576
+ """Test 1: Verify webhook endpoints are registered for walkers with @restspec(webhook=True)."""
1577
+ response = requests.get(f"{self.base_url}/openapi.json", timeout=5)
1578
+ assert response.status_code == 200
1579
+ schema = response.json()
1580
+ paths = schema.get("paths", {})
1581
+
1582
+ # PaymentReceived has @restspec(webhook=True) -> should have /webhook/ endpoint
1583
+ assert "/webhook/PaymentReceived" in paths, (
1584
+ f"Expected /webhook/PaymentReceived in paths: {list(paths.keys())}"
1585
+ )
1586
+ # MinimalWebhook has @restspec(webhook=True) -> should have /webhook/ endpoint
1587
+ assert "/webhook/MinimalWebhook" in paths, (
1588
+ f"Expected /webhook/MinimalWebhook in paths: {list(paths.keys())}"
1589
+ )
1590
+
1591
+ def test_normal_walker_not_in_webhook_endpoint(self) -> None:
1592
+ """Test 2: Verify normal walkers (without @restspec(webhook=True)) are NOT in /webhook/."""
1593
+ response = requests.get(f"{self.base_url}/openapi.json", timeout=5)
1594
+ assert response.status_code == 200
1595
+ schema = response.json()
1596
+ paths = schema.get("paths", {})
1597
+
1598
+ # NormalPayment does NOT have @restspec(webhook=True) -> should NOT have /webhook/ endpoint
1599
+ assert "/webhook/NormalPayment" not in paths, (
1600
+ "NormalPayment should NOT have webhook endpoint but found in paths"
1601
+ )
1602
+ # NormalPayment should be accessible via /walker/ endpoint
1603
+ assert "/walker/NormalPayment" in paths or "/walker/{walker_name}" in paths, (
1604
+ "NormalPayment should be accessible via /walker/ endpoint"
1605
+ )
1606
+
1607
+ def test_normal_walker_accessible_via_walker_endpoint(self) -> None:
1608
+ """Test 2b: Verify NormalPayment (no webhook restspec) works via /walker/ endpoint."""
1609
+ # Create user and get token
1610
+ username = f"normal_walker_user_{uuid.uuid4().hex[:8]}"
1611
+ register_response = requests.post(
1612
+ f"{self.base_url}/user/register",
1613
+ json={"username": username, "password": "password123"},
1614
+ timeout=10,
1615
+ )
1616
+ assert register_response.status_code == 201
1617
+ register_data = cast(
1618
+ dict[str, Any],
1619
+ self._extract_transport_response_data(register_response.json()),
1620
+ )
1621
+ token = register_data["token"]
1622
+
1623
+ # Call NormalPayment via /walker/ endpoint
1624
+ response = requests.post(
1625
+ f"{self.base_url}/walker/NormalPayment",
1626
+ json={
1627
+ "payment_id": "PAY-NORMAL-001",
1628
+ "order_id": "ORD-NORMAL-001",
1629
+ "amount": 50.00,
1630
+ "currency": "EUR",
1631
+ },
1632
+ headers={"Authorization": f"Bearer {token}"},
1633
+ timeout=10,
1634
+ )
1635
+
1636
+ assert response.status_code == 200, (
1637
+ f"Expected 200, got {response.status_code}: {response.text}"
1638
+ )
1639
+ data = cast(
1640
+ dict[str, Any], self._extract_transport_response_data(response.json())
1641
+ )
1642
+ assert "reports" in data
1643
+ report = data["reports"][0]
1644
+ assert report["status"] == "success"
1645
+ assert report["payment_id"] == "PAY-NORMAL-001"
1646
+ assert report["transport"] == "http"
1647
+
1648
+ def test_webhook_requires_api_key(self) -> None:
1649
+ """Test that webhook endpoints require API key authentication."""
1650
+ payload = json.dumps({})
1651
+
1652
+ # Try to call MinimalWebhook without API key
1653
+ response = requests.post(
1654
+ f"{self.base_url}/webhook/MinimalWebhook",
1655
+ data=payload,
1656
+ headers={"Content-Type": "application/json"},
1657
+ timeout=5,
1658
+ )
1659
+
1660
+ # Should fail - 401 (our custom error) or 422 (FastAPI validation for missing header)
1661
+ assert response.status_code in (401, 422), (
1662
+ f"Expected 401 or 422, got {response.status_code}: {response.text}"
1663
+ )
1664
+
1665
+ def test_webhook_invalid_api_key(self) -> None:
1666
+ """Test that webhook endpoints reject invalid API keys."""
1667
+ payload = json.dumps({})
1668
+
1669
+ # Try with invalid API key
1670
+ response = requests.post(
1671
+ f"{self.base_url}/webhook/MinimalWebhook",
1672
+ data=payload,
1673
+ headers={
1674
+ "Content-Type": "application/json",
1675
+ "X-API-Key": "invalid_key_12345",
1676
+ },
1677
+ timeout=5,
1678
+ )
1679
+
1680
+ # Should fail with 401 Unauthorized
1681
+ assert response.status_code == 401, (
1682
+ f"Expected 401, got {response.status_code}: {response.text}"
1683
+ )
1684
+
1685
+ def test_minimal_webhook_with_valid_api_key(self) -> None:
1686
+ """Test 3: MinimalWebhook (with @restspec(webhook=True)) works with valid API key."""
1687
+ # Create user and get auth token
1688
+ username = f"minimal_webhook_user_{uuid.uuid4().hex[:8]}"
1689
+ register_response = requests.post(
1690
+ f"{self.base_url}/user/register",
1691
+ json={"username": username, "password": "password123"},
1692
+ timeout=10,
1693
+ )
1694
+ assert register_response.status_code == 201
1695
+ register_data = cast(
1696
+ dict[str, Any],
1697
+ self._extract_transport_response_data(register_response.json()),
1698
+ )
1699
+ token = register_data["token"]
1700
+
1701
+ # Create API key
1702
+ api_key_response = requests.post(
1703
+ f"{self.base_url}/api-key/create",
1704
+ json={"name": "minimal_webhook_key", "expiry_days": 30},
1705
+ headers={"Authorization": f"Bearer {token}"},
1706
+ timeout=10,
1707
+ )
1708
+ assert api_key_response.status_code == 201, (
1709
+ f"Failed to create API key: {api_key_response.text}"
1710
+ )
1711
+ api_key_data = cast(
1712
+ dict[str, Any],
1713
+ self._extract_transport_response_data(api_key_response.json()),
1714
+ )
1715
+ api_key = api_key_data["api_key"]
1716
+
1717
+ # Call MinimalWebhook with valid API key (empty payload - no fields required)
1718
+ payload = json.dumps({})
1719
+ payload_bytes = payload.encode("utf-8")
1720
+ signature = self._generate_webhook_signature(payload_bytes, api_key)
1721
+ response = requests.post(
1722
+ f"{self.base_url}/webhook/MinimalWebhook",
1723
+ data=payload,
1724
+ headers={
1725
+ "Content-Type": "application/json",
1726
+ "X-API-Key": api_key,
1727
+ "X-Webhook-Signature": signature,
1728
+ },
1729
+ timeout=10,
1730
+ )
1731
+
1732
+ assert response.status_code == 200, (
1733
+ f"Expected 200, got {response.status_code}: {response.text}"
1734
+ )
1735
+ data = cast(
1736
+ dict[str, Any], self._extract_transport_response_data(response.json())
1737
+ )
1738
+ assert "reports" in data
1739
+ assert data["reports"][0]["status"] == "received"
1740
+ assert data["reports"][0]["transport"] == "webhook"
1741
+
1742
+ def test_webhook_payment_received_with_fields(self) -> None:
1743
+ """Test 1: PaymentReceived webhook walker with multiple fields + @restspec(webhook=True)."""
1744
+ # Create user and get API key
1745
+ username = f"payment_user_{uuid.uuid4().hex[:8]}"
1746
+ register_response = requests.post(
1747
+ f"{self.base_url}/user/register",
1748
+ json={"username": username, "password": "password123"},
1749
+ timeout=10,
1750
+ )
1751
+ assert register_response.status_code == 201
1752
+ register_data = cast(
1753
+ dict[str, Any],
1754
+ self._extract_transport_response_data(register_response.json()),
1755
+ )
1756
+ token = register_data["token"]
1757
+
1758
+ api_key_response = requests.post(
1759
+ f"{self.base_url}/api-key/create",
1760
+ json={"name": "payment_webhook_key", "expiry_days": 30},
1761
+ headers={"Authorization": f"Bearer {token}"},
1762
+ timeout=10,
1763
+ )
1764
+ assert api_key_response.status_code == 201
1765
+ api_key_data = cast(
1766
+ dict[str, Any],
1767
+ self._extract_transport_response_data(api_key_response.json()),
1768
+ )
1769
+ api_key = api_key_data["api_key"]
1770
+
1771
+ # Send payment webhook
1772
+ payload = json.dumps(
1773
+ {
1774
+ "payment_id": "PAY-12345",
1775
+ "order_id": "ORD-67890",
1776
+ "amount": 99.99,
1777
+ "currency": "USD",
1778
+ }
1779
+ )
1780
+ payload_bytes = payload.encode("utf-8")
1781
+ signature = self._generate_webhook_signature(payload_bytes, api_key)
1782
+
1783
+ response = requests.post(
1784
+ f"{self.base_url}/webhook/PaymentReceived",
1785
+ data=payload,
1786
+ headers={
1787
+ "Content-Type": "application/json",
1788
+ "X-API-Key": api_key,
1789
+ "X-Webhook-Signature": signature,
1790
+ },
1791
+ timeout=10,
1792
+ )
1793
+
1794
+ assert response.status_code == 200, (
1795
+ f"Expected 200, got {response.status_code}: {response.text}"
1796
+ )
1797
+ data = cast(
1798
+ dict[str, Any], self._extract_transport_response_data(response.json())
1799
+ )
1800
+ assert "reports" in data
1801
+ report = data["reports"][0]
1802
+ assert report["status"] == "success"
1803
+ assert report["payment_id"] == "PAY-12345"
1804
+ assert report["order_id"] == "ORD-67890"
1805
+ assert report["amount"] == 99.99
1806
+ assert report["currency"] == "USD"
1807
+
1808
+ def test_webhook_not_accessible_via_regular_walker_endpoint(self) -> None:
1809
+ """Test that webhook walkers (with @restspec(webhook=True)) are NOT accessible via /walker/."""
1810
+ # Create user and get token
1811
+ username = f"webhook_path_user_{uuid.uuid4().hex[:8]}"
1812
+ register_response = requests.post(
1813
+ f"{self.base_url}/user/register",
1814
+ json={"username": username, "password": "password123"},
1815
+ timeout=10,
1816
+ )
1817
+ assert register_response.status_code == 201
1818
+ register_data = cast(
1819
+ dict[str, Any],
1820
+ self._extract_transport_response_data(register_response.json()),
1821
+ )
1822
+ token = register_data["token"]
1823
+
1824
+ # Try to access PaymentReceived (webhook walker) via /walker/ endpoint
1825
+ response = requests.post(
1826
+ f"{self.base_url}/walker/PaymentReceived",
1827
+ json={
1828
+ "payment_id": "PAY-TEST",
1829
+ "order_id": "ORD-TEST",
1830
+ "amount": 10.00,
1831
+ },
1832
+ headers={"Authorization": f"Bearer {token}"},
1833
+ timeout=10,
1834
+ )
1835
+
1836
+ # Webhook walkers should not be accessible via /walker/ endpoint
1837
+ # In static mode: 404 (Not Found) or 405 (Method Not Allowed - no POST registered)
1838
+ # In dynamic mode: 400 (Bad Request - explicitly rejected)
1839
+ assert response.status_code in (400, 404, 405), (
1840
+ f"Expected 400/404/405, got {response.status_code}: {response.text}"
1841
+ )
1842
+
1843
+ def test_webhook_revoked_api_key(self) -> None:
1844
+ """Test that revoked API keys are rejected."""
1845
+ # Create user and get auth token
1846
+ username = f"webhook_revoke_user_{uuid.uuid4().hex[:8]}"
1847
+ register_response = requests.post(
1848
+ f"{self.base_url}/user/register",
1849
+ json={"username": username, "password": "password123"},
1850
+ timeout=10,
1851
+ )
1852
+ assert register_response.status_code == 201
1853
+ register_data = cast(
1854
+ dict[str, Any],
1855
+ self._extract_transport_response_data(register_response.json()),
1856
+ )
1857
+ token = register_data["token"]
1858
+
1859
+ # Create API key
1860
+ api_key_response = requests.post(
1861
+ f"{self.base_url}/api-key/create",
1862
+ json={"name": "key_to_revoke", "expiry_days": 30},
1863
+ headers={"Authorization": f"Bearer {token}"},
1864
+ timeout=10,
1865
+ )
1866
+ assert api_key_response.status_code == 201
1867
+ api_key_data = cast(
1868
+ dict[str, Any],
1869
+ self._extract_transport_response_data(api_key_response.json()),
1870
+ )
1871
+ api_key = api_key_data["api_key"]
1872
+ api_key_id = api_key_data["api_key_id"]
1873
+
1874
+ # Verify API key works with MinimalWebhook
1875
+ payload = json.dumps({})
1876
+ payload_bytes = payload.encode("utf-8")
1877
+ signature = self._generate_webhook_signature(payload_bytes, api_key)
1878
+ response = requests.post(
1879
+ f"{self.base_url}/webhook/MinimalWebhook",
1880
+ data=payload,
1881
+ headers={
1882
+ "Content-Type": "application/json",
1883
+ "X-API-Key": api_key,
1884
+ "X-Webhook-Signature": signature,
1885
+ },
1886
+ timeout=10,
1887
+ )
1888
+ assert response.status_code == 200
1889
+
1890
+ # Revoke the API key
1891
+ revoke_response = requests.delete(
1892
+ f"{self.base_url}/api-key/{api_key_id}",
1893
+ headers={"Authorization": f"Bearer {token}"},
1894
+ timeout=10,
1895
+ )
1896
+ assert revoke_response.status_code == 200, (
1897
+ f"Failed to revoke key: {revoke_response.text}"
1898
+ )
1899
+
1900
+ # Try to use revoked key (same payload and signature)
1901
+ response = requests.post(
1902
+ f"{self.base_url}/webhook/MinimalWebhook",
1903
+ data=payload,
1904
+ headers={
1905
+ "Content-Type": "application/json",
1906
+ "X-API-Key": api_key,
1907
+ "X-Webhook-Signature": signature,
1908
+ },
1909
+ timeout=10,
1910
+ )
1911
+
1912
+ # Should fail with 401
1913
+ assert response.status_code == 401, (
1914
+ f"Expected 401 for revoked key, got {response.status_code}: {response.text}"
1915
+ )
1916
+
1564
1917
 
1565
1918
  class TestJacScaleServeDevMode:
1566
1919
  """Test jac-scale serve with --dev mode (dynamic routing).
jac_scale/webhook.jac ADDED
@@ -0,0 +1,93 @@
1
+ """Webhook support for Jac Scale.
2
+
3
+ This module provides webhook-related functionality including:
4
+ - API key generation and management
5
+ - HMAC-SHA256 signature verification for webhook security
6
+ - Webhook configuration and utilities
7
+
8
+ API keys are generated per-user and can be used to authenticate webhook calls.
9
+ The API key is wrapped in a JWT for secure storage and validation.
10
+ """
11
+ import hmac;
12
+ import hashlib;
13
+ import secrets;
14
+ import jwt;
15
+ import from datetime { UTC, datetime, timedelta }
16
+ import from typing { Any }
17
+ import from pydantic { BaseModel, Field }
18
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
19
+ import from jac_scale.config_loader { get_scale_config }
20
+
21
+ # Load webhook configuration
22
+ glob _webhook_config = get_scale_config().get_webhook_config(),
23
+ WEBHOOK_SECRET = _webhook_config['secret'],
24
+ WEBHOOK_SIGNATURE_HEADER = _webhook_config['signature_header'],
25
+ WEBHOOK_VERIFY_SIGNATURE = _webhook_config['verify_signature'],
26
+ WEBHOOK_API_KEY_EXPIRY_DAYS = _webhook_config['api_key_expiry_days'];
27
+
28
+ """Pydantic model for API key creation request."""
29
+ class CreateApiKeyRequest(BaseModel) {
30
+ has name: str = Field(..., description='A friendly name for the API key'),
31
+ expiry_days: int | None = Field(
32
+ None, description='Number of days until expiry (default from config)'
33
+ );
34
+ }
35
+
36
+ """Pydantic model for API key response."""
37
+ class ApiKeyResponse(BaseModel) {
38
+ has api_key: str = Field(..., description='The generated API key'),
39
+ api_key_id: str = Field(..., description='Unique identifier for the API key'),
40
+ name: str = Field(..., description='Friendly name for the API key'),
41
+ created_at: str = Field(..., description='ISO timestamp of creation'),
42
+ expires_at: str | None = Field(
43
+ None, description='ISO timestamp of expiry, or null if no expiry'
44
+ );
45
+ }
46
+
47
+ """Webhook utilities for signature generation and verification."""
48
+ obj WebhookUtils {
49
+ """Generate HMAC-SHA256 signature for webhook payload."""
50
+ static def generate_signature(payload: bytes, secret: str) -> str;
51
+
52
+ """Verify HMAC-SHA256 signature for webhook payload."""
53
+ static def verify_signature(payload: bytes, signature: str, secret: str) -> bool;
54
+
55
+ """Generate a new API key."""
56
+ static def generate_api_key -> str;
57
+
58
+ """Create a JWT-wrapped API key token.
59
+
60
+ The API key is embedded in a JWT for secure validation and expiry management."""
61
+ static def create_api_key_token(
62
+ api_key_id: str, username: str, name: str, expiry_days: int | None = None
63
+ ) -> str;
64
+
65
+ """Validate an API key token and extract user information."""
66
+ static def validate_api_key(api_key: str) -> dict[str, str] | None;
67
+
68
+ """Extract signature from request header value."""
69
+ static def extract_signature(header_value: str) -> str;
70
+ }
71
+
72
+ """Manager for API keys associated with users."""
73
+ obj ApiKeyManager {
74
+ has _api_keys: dict[str, dict[str, Any]] = {}, # api_key_id -> key info
75
+ _user_keys: dict[str, list[str]] = {}; # username -> list of api_key_ids
76
+
77
+ """Create a new API key for a user."""
78
+ def create_api_key(
79
+ username: str, name: str, expiry_days: int | None = None
80
+ ) -> TransportResponse;
81
+
82
+ """List all API keys for a user."""
83
+ def list_api_keys(username: str) -> TransportResponse;
84
+
85
+ """Revoke an API key."""
86
+ def revoke_api_key(username: str, api_key_id: str) -> TransportResponse;
87
+
88
+ """Validate an API key and return the associated username."""
89
+ def validate_api_key(api_key: str) -> str | None;
90
+
91
+ """Check if an API key ID exists and is not revoked."""
92
+ def is_key_active(api_key_id: str) -> bool;
93
+ }
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jac-scale
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Author-email: Jason Mars <jason@mars.ninja>
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: jaclang>=0.9.12
7
+ Requires-Dist: jaclang>=0.9.13
8
8
  Requires-Dist: python-dotenv<2.0.0,>=1.2.1
9
9
  Requires-Dist: docker<8.0.0,>=7.1.0
10
10
  Requires-Dist: kubernetes<35.0.0,>=34.1.0
@@ -12,7 +12,7 @@ Requires-Dist: pymongo<5.0.0,>=4.15.4
12
12
  Requires-Dist: redis<8.0.0,>=7.1.0
13
13
  Requires-Dist: fastapi<0.122.0,>=0.121.3
14
14
  Requires-Dist: uvicorn<0.39.0,>=0.38.0
15
- Requires-Dist: pyjwt
15
+ Requires-Dist: pyjwt<2.11.0,>=2.10.1
16
16
  Requires-Dist: fastapi-sso<1.0.0,>=0.18.0
17
17
  Requires-Dist: python-multipart<1.0.0,>=0.0.21
18
18
 
@@ -491,7 +491,7 @@ async walker FetchData {
491
491
  | `MONGODB_URI` | URL of MongoDB database | - |
492
492
  | `REDIS_URL` | URL of Redis database | - |
493
493
  | `JWT_EXP_DELTA_DAYS` | Number of days until JWT token expires | `7` |
494
- | `JWT_SECRET` | Secret key used for JWT token signing and verification | `'supersecretkey'` |
494
+ | `JWT_SECRET` | Secret key used for JWT token signing and verification | `'supersecretkey_for_testing_only!'` |
495
495
  | `JWT_ALGORITHM` | Algorithm used for JWT token encoding/decoding | `'HS256'` |
496
496
  | `SSO_HOST` | SSO host URL | `'http://localhost:8000/sso'` |
497
497
  | `SSO_GOOGLE_CLIENT_ID` | Google OAuth client ID | - |
@@ -1,14 +1,15 @@
1
1
  jac_scale/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- jac_scale/config_loader.jac,sha256=oOOOFcFfEP89Y3A-AI4cLqrEaO6g0jJqZglBsCoKM84,1222
2
+ jac_scale/config_loader.jac,sha256=fG1dN9AS54bzNL5XvPHNS4oMw8kknZnlAZ7ByPT0APU,1298
3
3
  jac_scale/context.jac,sha256=SIqQineAlLHJV3yWtYkeTIfcnqS0Oj15n8CWVSIqwJ8,529
4
4
  jac_scale/google_sso_provider.jac,sha256=UUTDgQrBHXp0eqHxiw7v_VkQyFMMza0UDM6S4T5e4OM,2473
5
5
  jac_scale/memory_hierarchy.jac,sha256=GuydhwujsH71TAkxeJ6Wfutp4v6GjDj23vGMreRj4yg,4533
6
6
  jac_scale/plugin.jac,sha256=E1Ou_sQ8RV35emY9-gM295Uu-Eg4w4qSEJYm3TTYsWY,9585
7
- jac_scale/plugin_config.jac,sha256=Gxvyy8V0uP1I4XARfHaHqoz0oeZ0LbHibri3gi5Odp8,8700
8
- jac_scale/serve.jac,sha256=IU1a6ijW19FCBx53P_d-fzgukuZzS6MDTdazkhbtuzQ,4430
7
+ jac_scale/plugin_config.jac,sha256=6_cTMzjDhr-7fGh2RMM5LCfjXlfKduGyFs0S0PnHKXQ,8718
8
+ jac_scale/serve.jac,sha256=8xMOHX3X-sJFx6yENolL-7VM7Km1Mg5BPkUtMLUl9O4,5546
9
9
  jac_scale/sso_provider.jac,sha256=Kky8cxm5gR2Sbg-CFrWTILKSZrV6Ju-DCEgfG4Akxb8,2092
10
10
  jac_scale/user_manager.jac,sha256=UBApt_znhVEyxtfAdJ9sp_gslkHel4GGwXCBcNen_as,1932
11
11
  jac_scale/utils.jac,sha256=zpxA08_NlDDSd1oVlbknQ_UOwHWGBbhzjmT_u6UYN04,491
12
+ jac_scale/webhook.jac,sha256=lTpevPoe-CEcj0-MAd8c1fHK7jGUL3YnN4_ouMeq4Ic,3660
12
13
  jac_scale/abstractions/database_provider.jac,sha256=NZbFmcESulYsmG-27f8GY9nxVs3JflGIEb3aDDOIPu0,1454
13
14
  jac_scale/abstractions/deployment_target.jac,sha256=5rJOd25FUXZZCNmDSUkyMljcXTAVJJDmelKw8WdPZm0,2071
14
15
  jac_scale/abstractions/image_registry.jac,sha256=UCOwwuts5e1emsdDIlHJOiyW2ePA7k7D8UJdzd2kN10,1386
@@ -22,17 +23,18 @@ jac_scale/factories/deployment_factory.jac,sha256=1pzQ2HkqpKaeoycbRxdZ05McgTLpgj
22
23
  jac_scale/factories/registry_factory.jac,sha256=qWgwEH75EtjXGj3d9NGeg7D9Yx2S_RW9udlCFvOOCqU,1210
23
24
  jac_scale/factories/storage_factory.jac,sha256=FVlx-f1BOq6H1yLWXT7OLSG-ZCENvlBPNr0OwFdo-VU,2661
24
25
  jac_scale/factories/utility_factory.jac,sha256=64gMv_N_pFeq5E7z3gXrLIqeRGS_RGIF5eLT8RxNl_I,1262
25
- jac_scale/impl/config_loader.impl.jac,sha256=j8YngI4mdcBKCBtly-8vN0WYXDwzdhwZ6bmmMc9Kza4,4769
26
- jac_scale/impl/context.impl.jac,sha256=D8B7K-Y1VdbqBfGjYtGA5BDBiizlL8MmvAqxO8xMLwE,1106
26
+ jac_scale/impl/config_loader.impl.jac,sha256=gIcOowYuoMvpilRK6vhAGf5ULYttrkh19fB5zP7KTdI,5668
27
+ jac_scale/impl/context.impl.jac,sha256=3M33iA9L2SCIMT0rQTvb8MkveNCuTEjc7rNknK4Hruk,1071
27
28
  jac_scale/impl/memory_hierarchy.main.impl.jac,sha256=Jc4Cy-5cy0TtK2uQ6rdYruxa0ZfZKSYgo3jOoXv4ER4,2339
28
29
  jac_scale/impl/memory_hierarchy.mongo.impl.jac,sha256=APh7YloOj7bWGtv6DTBm7ekX13w_fectbZiFY0btwWU,6626
29
30
  jac_scale/impl/memory_hierarchy.redis.impl.jac,sha256=x27TzeBI87vz72s7esQvFz8EPk3EfhVMNKU8sP4s2hk,4900
30
- jac_scale/impl/serve.impl.jac,sha256=UX7GIoB27kHeyq1JxtkM4CYC1BnhGpipCu0qOGaePRM,61376
31
+ jac_scale/impl/serve.impl.jac,sha256=zzWAF0PukDMdKKFmiwP-cupxrCA8YtZ_leYY-Qzcnkc,85767
31
32
  jac_scale/impl/user_manager.impl.jac,sha256=7xGoLMweQuoVW-na7XbK3bctfUHaSEnT9nB7DLgSlgw,11817
33
+ jac_scale/impl/webhook.impl.jac,sha256=87IZn9nfSaNXX9RYXVJRJEz-3xzznpC6RhVBoJJA2X8,7504
32
34
  jac_scale/jserver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
35
  jac_scale/jserver/jfast_api.jac,sha256=Ez7EZ883TS9IIACzK8QMqTgVTtNoaOenbmwNKnekBuc,5561
34
36
  jac_scale/jserver/jserver.jac,sha256=8gWh22nX9Z5rJp3D1AxOSU8lBhoX4-kBZ0uvJPPNqX8,3356
35
- jac_scale/jserver/impl/jfast_api.impl.jac,sha256=fJZDFKbFYhpYqIcauPNPd5tpRHx_Xd1DXo49-vTSXQA,26766
37
+ jac_scale/jserver/impl/jfast_api.impl.jac,sha256=bnxoReOOntQL0FHDgLSoaXtQRtCYOOEVWhq-lAXeqI0,26931
36
38
  jac_scale/jserver/impl/jserver.impl.jac,sha256=4NOzfys4WQmGt6vttTTh6By12hEJqdvOzCZyCBsUQKk,2224
37
39
  jac_scale/providers/database/kubernetes_mongo.jac,sha256=nP43b-ePR8XInhhk7gzdvJvOJMZcTZanm1VALdiCqXA,4952
38
40
  jac_scale/providers/database/kubernetes_redis.jac,sha256=qF9HkNLLgiymxiCXbmETbVVDs5Wqs1m4s3O6xFR7jRA,3778
@@ -51,18 +53,18 @@ jac_scale/tests/test_file_upload.py,sha256=JooF-9rgwOqtNkWRfvfHu4W5x-gzk91bHVi13
51
53
  jac_scale/tests/test_hooks.py,sha256=TzikM6dd1uKg1G2L1MtgXYzzjY6YhrbAQXZah-TO33Y,1366
52
54
  jac_scale/tests/test_k8s_utils.py,sha256=Oqocwl4m2IFgM27Lgsi96WJeHS8hnx9WDOcxq9YkqJU,4613
53
55
  jac_scale/tests/test_memory_hierarchy.py,sha256=q_DsKyPIuQymab_sEtXojDW0GOyunRxZJsNTJDrGA7s,8324
54
- jac_scale/tests/test_restspec.py,sha256=ynkDlu9mHl98hUL49UYwcJNXbsLhBd8yPL9TTw12xN0,6582
55
- jac_scale/tests/test_serve.py,sha256=LsLgGGS5_52wtB0EyBEsIzX6KwIQ4m5hTVoOq3dYnWc,67299
56
+ jac_scale/tests/test_restspec.py,sha256=fYNpQi7ZhV7KnSoJQ4xH1tJIQeayaeCvFS5GBLAZO1o,10853
57
+ jac_scale/tests/test_serve.py,sha256=xKPjQJLdtWaCISlP3Kirf3A4i1Fxubej_h5oLklKSY8,81244
56
58
  jac_scale/tests/test_sso.py,sha256=9h2AUC9JBM74O6xfpyIcl98iXyZWcDIGdKRtlb_5uqo,28104
57
59
  jac_scale/tests/test_storage.py,sha256=Y7CIbKrH3OmJQlBeV9A965-7TtxL6nuZtfkfoeqIeS4,10514
58
- jac_scale/tests/fixtures/test_api.jac,sha256=78tw_0lFR2k7uI0nrU_e8fdJJOcOG2z0J5C47MQ42DU,4744
59
- jac_scale/tests/fixtures/test_restspec.jac,sha256=bE8-7qyDXkNGGhv0HqQCKFpLWCDa5ciRzrJS-bkqWDo,1173
60
+ jac_scale/tests/fixtures/test_api.jac,sha256=aqfvy4wgc0qPTM9_NYvXUcpemNTSdwW0tp432sdHZI4,6680
61
+ jac_scale/tests/fixtures/test_restspec.jac,sha256=oFqACGtuoGz1xsURAaeXrQe7hkMI-W8hL5IKFDTQHKU,2511
60
62
  jac_scale/tests/fixtures/todo_app.jac,sha256=beI6AiRutmXsXxuzU9nIZTE-AuoBBP-WqbRA2ux1pN4,1216
61
63
  jac_scale/tests/fixtures/scale-feats/main.jac,sha256=QN8Opodha9jLWdRGCASByIogePr1XKphgKjyE9Pp73c,4205
62
64
  jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac,sha256=e8tvNzW_QikFJMSTcbkziKP69NF00DqQVxcjLPkAW-w,903
63
65
  jac_scale/utilities/loggers/standard_logger.jac,sha256=6XL5ETAOBwbsFCOp0VN_7TOnqcQDmbLZVzubA-JR3vA,1376
64
- jac_scale-0.1.3.dist-info/METADATA,sha256=zRQUUOCa0B6p2qC4Fo-sptUZbayDW85aS51iX1SvSI4,20457
65
- jac_scale-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
66
- jac_scale-0.1.3.dist-info/entry_points.txt,sha256=n-Wm8JEtGOqy_IY_kgIOi3-uYnuVK-iWsvKiLkxlG4E,105
67
- jac_scale-0.1.3.dist-info/top_level.txt,sha256=PpgR0R8z9qoFbSser2K20r5Is4K6TxVwguoN6LfTEKU,10
68
- jac_scale-0.1.3.dist-info/RECORD,,
66
+ jac_scale-0.1.4.dist-info/METADATA,sha256=F_sIgqYZVaPGz7cPpx-Sc-EqI0cNX43nEkFWQiUjU5w,20491
67
+ jac_scale-0.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
68
+ jac_scale-0.1.4.dist-info/entry_points.txt,sha256=n-Wm8JEtGOqy_IY_kgIOi3-uYnuVK-iWsvKiLkxlG4E,105
69
+ jac_scale-0.1.4.dist-info/top_level.txt,sha256=PpgR0R8z9qoFbSser2K20r5Is4K6TxVwguoN6LfTEKU,10
70
+ jac_scale-0.1.4.dist-info/RECORD,,