@jskit-ai/rewarded-core 0.1.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/docs/protecting-server-actions.md +190 -0
- package/migrations/rewarded_provider_configs_initial.cjs +27 -0
- package/migrations/rewarded_rules_initial.cjs +31 -0
- package/migrations/rewarded_unlock_receipts_initial.cjs +35 -0
- package/migrations/rewarded_watch_sessions_initial.cjs +36 -0
- package/package.json +130 -0
- package/src/server/RewardedCoreProvider.js +44 -0
- package/src/server/RewardedResources.js +69 -0
- package/src/server/actions.js +152 -0
- package/src/server/inputSchemas.js +394 -0
- package/src/server/registerRoutes.js +167 -0
- package/src/server/service.js +566 -0
- package/src/server/support/requireRewardedUnlock.js +142 -0
- package/src/shared/index.js +12 -0
- package/src/shared/rewardedProviderConfigResource.js +95 -0
- package/src/shared/rewardedRuleResource.js +136 -0
- package/src/shared/rewardedUnlockReceiptResource.js +113 -0
- package/src/shared/rewardedWatchSessionResource.js +139 -0
- package/test/featureRuntime.test.js +91 -0
- package/test/requireRewardedUnlock.test.js +274 -0
- package/test/routes.test.js +239 -0
- package/test/service.test.js +357 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @jskit-ai/rewarded-core
|
|
2
|
+
|
|
3
|
+
Server runtime for Rewarded unlock gates.
|
|
4
|
+
|
|
5
|
+
## Package Shape
|
|
6
|
+
|
|
7
|
+
This package installs:
|
|
8
|
+
|
|
9
|
+
- four CRUD-owned server providers, one per persisted table
|
|
10
|
+
- one workflow provider for the rewarded gate API
|
|
11
|
+
|
|
12
|
+
The persisted tables are:
|
|
13
|
+
|
|
14
|
+
- `rewarded_rules`
|
|
15
|
+
- `rewarded_provider_configs`
|
|
16
|
+
- `rewarded_watch_sessions`
|
|
17
|
+
- `rewarded_unlock_receipts`
|
|
18
|
+
|
|
19
|
+
The package keeps the CRUD ownership strict:
|
|
20
|
+
|
|
21
|
+
- rules and provider configs are `workspace`-owned
|
|
22
|
+
- watch sessions and unlock receipts are `workspace_user`-owned
|
|
23
|
+
|
|
24
|
+
Every owned row carries direct owner columns. The module does not rely on inherited ownership through parent joins.
|
|
25
|
+
|
|
26
|
+
## What It Does
|
|
27
|
+
|
|
28
|
+
The workflow provider exposes four workspace-scoped app routes:
|
|
29
|
+
|
|
30
|
+
- `GET /api/w/:workspaceSlug/rewarded/current`
|
|
31
|
+
- `POST /api/w/:workspaceSlug/rewarded/start`
|
|
32
|
+
- `POST /api/w/:workspaceSlug/rewarded/grant`
|
|
33
|
+
- `POST /api/w/:workspaceSlug/rewarded/close`
|
|
34
|
+
|
|
35
|
+
These are plain workflow endpoints, not CRUD JSON:API endpoints.
|
|
36
|
+
|
|
37
|
+
The flow is:
|
|
38
|
+
|
|
39
|
+
1. `current` decides whether the gate is enabled, blocked, or already unlocked.
|
|
40
|
+
2. `start` creates a watch session when a reward is required.
|
|
41
|
+
3. `grant` marks the session rewarded and creates an unlock receipt.
|
|
42
|
+
4. `close` closes a started session without granting access.
|
|
43
|
+
|
|
44
|
+
Day 0 is intentionally app-surface-only for the rewarded workflow. Rules and provider configs should therefore use `surface = "app"` for the active gate rows.
|
|
45
|
+
|
|
46
|
+
## Required Data
|
|
47
|
+
|
|
48
|
+
Day-0 configuration lives in the CRUD-owned tables.
|
|
49
|
+
|
|
50
|
+
At minimum, apps need:
|
|
51
|
+
|
|
52
|
+
- a `rewarded_rules` row for the target `gateKey`
|
|
53
|
+
- a matching enabled `rewarded_provider_configs` row for the surface
|
|
54
|
+
|
|
55
|
+
Important config fields:
|
|
56
|
+
|
|
57
|
+
- `rewarded_rules.gate_key`
|
|
58
|
+
- `rewarded_rules.surface`
|
|
59
|
+
- `rewarded_rules.unlock_minutes`
|
|
60
|
+
- `rewarded_rules.cooldown_minutes`
|
|
61
|
+
- `rewarded_rules.daily_limit`
|
|
62
|
+
- `rewarded_provider_configs.surface`
|
|
63
|
+
- `rewarded_provider_configs.placement`
|
|
64
|
+
- `rewarded_provider_configs.provider`
|
|
65
|
+
|
|
66
|
+
The application selects `provider` and `placement` for its delivery adapter.
|
|
67
|
+
The workflow does not interpret them or choose a default provider. For the
|
|
68
|
+
Google application pattern use `provider = "google-publisher-tag"` and the ad unit
|
|
69
|
+
path as `placement`.
|
|
70
|
+
|
|
71
|
+
## Protecting Server Features
|
|
72
|
+
|
|
73
|
+
Protected server mutations should use the exported helper:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { requireRewardedUnlock } from "@jskit-ai/rewarded-core/server/requireRewardedUnlock";
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The dedicated manual page is:
|
|
80
|
+
|
|
81
|
+
- [docs/protecting-server-actions.md](docs/protecting-server-actions.md)
|
|
82
|
+
|
|
83
|
+
That page shows the exact service and provider wiring pattern.
|
|
84
|
+
|
|
85
|
+
## Grant authorization
|
|
86
|
+
|
|
87
|
+
The application must supply capability `rewarded.grant-policy` with an
|
|
88
|
+
`authorizeGrant({ session, context, trx })` function. Direct service composition
|
|
89
|
+
supplies the same function as `authorizeGrant`. Missing policy fails at setup;
|
|
90
|
+
only the literal result `true` permits a grant. False, absent or other results
|
|
91
|
+
reject it before any watch-session update or receipt creation.
|
|
92
|
+
|
|
93
|
+
The workflow loads the session through its owned repository, then passes that
|
|
94
|
+
stored session, the server request context and the current transaction to policy.
|
|
95
|
+
It never accepts a client approval flag or replacement gate identity. The policy
|
|
96
|
+
runs for repeated grant requests too. Keep policy local and bounded within the
|
|
97
|
+
transaction; use the application's trusted eligibility or verification records.
|
|
98
|
+
A policy exception propagates as failure rather than silently allowing a grant.
|
|
99
|
+
|
|
100
|
+
The Google browser pattern supplies delivery only. Its callbacks are not a
|
|
101
|
+
server authorization implementation. The application must decide which reward
|
|
102
|
+
claims it accepts; the library supplies neither provider-proof verification nor
|
|
103
|
+
a permissive default.
|
|
104
|
+
|
|
105
|
+
## Policy Model
|
|
106
|
+
|
|
107
|
+
This module is designed for rewarded unlocks, not for blocking all normal app use on boot.
|
|
108
|
+
|
|
109
|
+
Recommended usage:
|
|
110
|
+
|
|
111
|
+
- unlock bonus actions
|
|
112
|
+
- unlock extra quota
|
|
113
|
+
- unlock a temporary feature window
|
|
114
|
+
|
|
115
|
+
Do not treat it as a hard requirement for baseline product use unless the ad provider policy clearly allows that.
|
|
116
|
+
|
|
117
|
+
## Install Notes
|
|
118
|
+
|
|
119
|
+
The package declares its four initial schema migrations under `migrations/`.
|
|
120
|
+
Run them through the application's existing database migration operation.
|
|
121
|
+
|
|
122
|
+
The package does not add day-0 settings pages automatically. Configuration can stay manual or be layered with app-specific UI later.
|
|
123
|
+
|
|
124
|
+
## Manual V0 migration
|
|
125
|
+
|
|
126
|
+
Replace `@jskit-ai/google-rewarded-core` with `@jskit-ai/rewarded-core` and
|
|
127
|
+
`@jskit-ai/google-rewarded-web` with `@jskit-ai/rewarded-web`. Update imports
|
|
128
|
+
(`requireRewardedUnlock`, `createRewardedRuntime`, `useRewardedRuntime`),
|
|
129
|
+
`Rewarded*` provider names, `rewarded.*` server capabilities, `client.rewarded`
|
|
130
|
+
and `/api/w/:workspaceSlug/rewarded/*` routes. Register application-owned
|
|
131
|
+
`client.rewarded-delivery` and `rewarded.grant-policy` providers as described
|
|
132
|
+
in these packages. There is no implicit grant approval for older apps.
|
|
133
|
+
|
|
134
|
+
For disposable prerelease data, recreate the four tables through the new
|
|
135
|
+
migrations. If retaining data, stop writers, back up the database and manually
|
|
136
|
+
rename each `google_rewarded_*` table to `rewarded_*`, preserving IDs, owner
|
|
137
|
+
columns and foreign-key relationships. In the provider configuration table,
|
|
138
|
+
rename `ad_unit_path` to `placement` and replace `script_mode` with required
|
|
139
|
+
`provider` values chosen for the application; existing GPT rows use
|
|
140
|
+
`google-publisher-tag`. Remove the old column/default. Reconcile the application's
|
|
141
|
+
migration records so initial table creation is not replayed against retained
|
|
142
|
+
schema; verify indexes, constraints and ownership with its database tools before
|
|
143
|
+
resuming writers. Decide explicitly whether to retain active watch sessions and
|
|
144
|
+
unlock receipts. No library migration shim, alias, old-table reader or dual write
|
|
145
|
+
performs this conversion.
|
|
146
|
+
|
|
147
|
+
The package manifest lists application-owned inputs in both `capabilities.requires`
|
|
148
|
+
and `capabilities.applicationRequires`. The latter identifies who supplies them;
|
|
149
|
+
it does not make the runtime dependency optional or provide a default.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Protecting Server Actions With `requireRewardedUnlock()`
|
|
2
|
+
|
|
3
|
+
This is the manual page for the server helper exported by:
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
@jskit-ai/rewarded-core/server/requireRewardedUnlock
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Use this helper when a feature is:
|
|
10
|
+
|
|
11
|
+
- allowed in principle for the actor
|
|
12
|
+
- but temporarily gated behind a rewarded unlock
|
|
13
|
+
|
|
14
|
+
Do not use it as a permission system.
|
|
15
|
+
|
|
16
|
+
The correct split is:
|
|
17
|
+
|
|
18
|
+
- permissions decide whether the actor may ever do the thing
|
|
19
|
+
- rewarded gates decide whether the actor must unlock the thing right now
|
|
20
|
+
|
|
21
|
+
## What The Helper Does
|
|
22
|
+
|
|
23
|
+
`requireRewardedUnlock()` calls `rewarded.core.service.getCurrentState(...)` and then enforces one simple rule:
|
|
24
|
+
|
|
25
|
+
- if the gate is already unlocked, it returns the gate state
|
|
26
|
+
- if the gate is not configured and `requireConfigured` is not enabled, it returns the gate state
|
|
27
|
+
- otherwise it throws an `AppError`
|
|
28
|
+
|
|
29
|
+
This means the helper is a truth-enforcement seam for protected server operations.
|
|
30
|
+
|
|
31
|
+
The UI should still call the web runtime first for user experience, but the protected server operation must also check the gate so users cannot bypass it by calling the endpoint directly.
|
|
32
|
+
|
|
33
|
+
## Import Path
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import { requireRewardedUnlock } from "@jskit-ai/rewarded-core/server/requireRewardedUnlock";
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Basic Service Pattern
|
|
40
|
+
|
|
41
|
+
Inject `rewarded.core.service` into your own feature service, then call the helper before the protected mutation.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { requireRewardedUnlock } from "@jskit-ai/rewarded-core/server/requireRewardedUnlock";
|
|
47
|
+
|
|
48
|
+
function createProgressLoggingService({
|
|
49
|
+
rewardedService,
|
|
50
|
+
workoutLogRepository
|
|
51
|
+
} = {}) {
|
|
52
|
+
async function logProgress(input = {}, options = {}) {
|
|
53
|
+
await requireRewardedUnlock(
|
|
54
|
+
rewardedService,
|
|
55
|
+
{
|
|
56
|
+
gateKey: "progress-logging",
|
|
57
|
+
workspaceSlug: input.workspaceSlug
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
context: options.context,
|
|
61
|
+
errorMessage: "Watch a rewarded ad before logging progress."
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return workoutLogRepository.create(input, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
logProgress
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Provider Wiring Pattern
|
|
75
|
+
|
|
76
|
+
Inject the rewarded service from the container the same way as any other JSKIT runtime service.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
app.service(
|
|
82
|
+
"convict.progress-logging.service",
|
|
83
|
+
(scope) => createProgressLoggingService({
|
|
84
|
+
rewardedService: scope.make("rewarded.core.service"),
|
|
85
|
+
workoutLogRepository: scope.make("convict.workout-log.repository")
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Exact Helper Signature
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
await requireRewardedUnlock(
|
|
94
|
+
rewardedService,
|
|
95
|
+
{
|
|
96
|
+
gateKey,
|
|
97
|
+
workspaceSlug
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
context,
|
|
101
|
+
requireConfigured,
|
|
102
|
+
errorCode,
|
|
103
|
+
errorMessage
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Arguments:
|
|
109
|
+
|
|
110
|
+
- `rewardedService`
|
|
111
|
+
- must expose `getCurrentState(input, { context })`
|
|
112
|
+
- first object
|
|
113
|
+
- `gateKey`: required gate identifier
|
|
114
|
+
- `workspaceSlug`: required for the current workspace gate
|
|
115
|
+
- second object
|
|
116
|
+
- `context`: the normal JSKIT action/service context
|
|
117
|
+
- `requireConfigured`: fail closed when no rule/provider config exists
|
|
118
|
+
- `errorCode`: optional override for the thrown `AppError.code`
|
|
119
|
+
- `errorMessage`: optional override for the thrown `AppError.message`
|
|
120
|
+
|
|
121
|
+
## Default Behavior
|
|
122
|
+
|
|
123
|
+
The helper is intentionally permissive when the rewarded gate is not configured.
|
|
124
|
+
|
|
125
|
+
By default:
|
|
126
|
+
|
|
127
|
+
- `reason = "rule-not-configured"` passes
|
|
128
|
+
- `reason = "provider-not-configured"` passes
|
|
129
|
+
|
|
130
|
+
Why:
|
|
131
|
+
|
|
132
|
+
- day-0 apps may install the package before creating live rules/config rows
|
|
133
|
+
- you do not want every protected feature to break during setup or partial rollout
|
|
134
|
+
|
|
135
|
+
If you want the protected operation to fail closed until configuration exists, set:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
requireConfigured: true
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## When The Helper Throws
|
|
142
|
+
|
|
143
|
+
The helper throws `AppError` with `details.rewardedGate` containing the full gate state.
|
|
144
|
+
|
|
145
|
+
Important built-in failure cases:
|
|
146
|
+
|
|
147
|
+
- `reward-required`
|
|
148
|
+
- status: `423`
|
|
149
|
+
- code: `rewarded_unlock_required`
|
|
150
|
+
- `cooldown-active`
|
|
151
|
+
- status: `423`
|
|
152
|
+
- code: `rewarded_cooldown_active`
|
|
153
|
+
- `daily-limit-reached`
|
|
154
|
+
- status: `423`
|
|
155
|
+
- code: `rewarded_daily_limit_reached`
|
|
156
|
+
- `rule-not-configured` or `provider-not-configured` with `requireConfigured: true`
|
|
157
|
+
- status: `503`
|
|
158
|
+
- code: `rewarded_not_configured`
|
|
159
|
+
|
|
160
|
+
## What The Helper Returns
|
|
161
|
+
|
|
162
|
+
On success, the helper returns the gate state from `rewardedService.getCurrentState(...)`.
|
|
163
|
+
|
|
164
|
+
That lets your service inspect the unlock window if it wants to log or branch on it, for example:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
const gateState = await requireRewardedUnlock(...);
|
|
168
|
+
const unlockedUntil = gateState.unlock?.unlockedUntil || null;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Recommended Calling Order
|
|
172
|
+
|
|
173
|
+
For a protected feature operation:
|
|
174
|
+
|
|
175
|
+
1. run normal permission / ownership checks
|
|
176
|
+
2. run `requireRewardedUnlock(...)`
|
|
177
|
+
3. perform the real mutation
|
|
178
|
+
|
|
179
|
+
Do not invert that order.
|
|
180
|
+
|
|
181
|
+
## What Not To Do
|
|
182
|
+
|
|
183
|
+
Do not:
|
|
184
|
+
|
|
185
|
+
- model rewarded gates as permissions
|
|
186
|
+
- skip the server check because the client already opened the gate
|
|
187
|
+
- rely on route visibility as the rewarded gate
|
|
188
|
+
- duplicate the raw `reason` checks in every feature service
|
|
189
|
+
|
|
190
|
+
Use the helper instead of open-coding that logic.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const TABLE_NAME = "rewarded_provider_configs";
|
|
2
|
+
|
|
3
|
+
exports.up = async function up(knex) {
|
|
4
|
+
const hasCrudTable = await knex.schema.hasTable(TABLE_NAME);
|
|
5
|
+
if (hasCrudTable) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
await knex.schema.createTable(TABLE_NAME, (table) => {
|
|
10
|
+
table.bigIncrements("id").unsigned().primary();
|
|
11
|
+
table.bigInteger("workspace_id").unsigned().notNullable();
|
|
12
|
+
table.string("surface", 64).notNullable();
|
|
13
|
+
table.boolean("enabled").notNullable().defaultTo(true);
|
|
14
|
+
table.string("placement", 255).notNullable();
|
|
15
|
+
table.string("provider", 64).notNullable();
|
|
16
|
+
table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
|
|
17
|
+
table.timestamp("updated_at").notNullable().defaultTo(knex.raw("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"));
|
|
18
|
+
table.index(["workspace_id"], "idx_rewarded_provider_configs_workspace");
|
|
19
|
+
table.unique(["workspace_id","surface"], "uq_rewarded_provider_configs_workspace_surface");
|
|
20
|
+
table.foreign(["workspace_id"], "fk_rewarded_provider_configs_workspace").references(["id"]).inTable("workspaces").onUpdate("RESTRICT").onDelete("CASCADE");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
exports.down = async function down(knex) {
|
|
26
|
+
await knex.schema.dropTableIfExists(TABLE_NAME);
|
|
27
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const TABLE_NAME = "rewarded_rules";
|
|
2
|
+
|
|
3
|
+
exports.up = async function up(knex) {
|
|
4
|
+
const hasCrudTable = await knex.schema.hasTable(TABLE_NAME);
|
|
5
|
+
if (hasCrudTable) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
await knex.schema.createTable(TABLE_NAME, (table) => {
|
|
10
|
+
table.bigIncrements("id").unsigned().primary();
|
|
11
|
+
table.bigInteger("workspace_id").unsigned().notNullable();
|
|
12
|
+
table.string("gate_key", 120).notNullable();
|
|
13
|
+
table.string("surface", 64).notNullable();
|
|
14
|
+
table.boolean("enabled").notNullable().defaultTo(true);
|
|
15
|
+
table.integer("unlock_minutes").unsigned().notNullable().defaultTo(30);
|
|
16
|
+
table.integer("cooldown_minutes").unsigned().notNullable().defaultTo(0);
|
|
17
|
+
table.integer("daily_limit").unsigned().nullable();
|
|
18
|
+
table.string("title", 160).notNullable().defaultTo("");
|
|
19
|
+
table.text("description").nullable();
|
|
20
|
+
table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
|
|
21
|
+
table.timestamp("updated_at").notNullable().defaultTo(knex.raw("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"));
|
|
22
|
+
table.index(["workspace_id"], "idx_rewarded_rules_workspace");
|
|
23
|
+
table.unique(["workspace_id","gate_key"], "uq_rewarded_rules_workspace_gate");
|
|
24
|
+
table.foreign(["workspace_id"], "fk_rewarded_rules_workspace").references(["id"]).inTable("workspaces").onUpdate("RESTRICT").onDelete("CASCADE");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
exports.down = async function down(knex) {
|
|
30
|
+
await knex.schema.dropTableIfExists(TABLE_NAME);
|
|
31
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const TABLE_NAME = "rewarded_unlock_receipts";
|
|
2
|
+
|
|
3
|
+
exports.up = async function up(knex) {
|
|
4
|
+
const hasCrudTable = await knex.schema.hasTable(TABLE_NAME);
|
|
5
|
+
if (hasCrudTable) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
await knex.schema.createTable(TABLE_NAME, (table) => {
|
|
10
|
+
table.bigIncrements("id").unsigned().primary();
|
|
11
|
+
table.bigInteger("workspace_id").unsigned().notNullable();
|
|
12
|
+
table.bigInteger("user_id").unsigned().notNullable();
|
|
13
|
+
table.string("gate_key", 120).notNullable();
|
|
14
|
+
table.bigInteger("provider_config_id").unsigned().nullable();
|
|
15
|
+
table.bigInteger("watch_session_id").unsigned().nullable();
|
|
16
|
+
table.timestamp("granted_at").notNullable().defaultTo(knex.fn.now());
|
|
17
|
+
table.timestamp("unlocked_until").notNullable();
|
|
18
|
+
table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
|
|
19
|
+
table.index(["user_id"], "fk_rewarded_unlock_receipts_user");
|
|
20
|
+
table.index(["gate_key"], "idx_rewarded_unlock_receipts_gate");
|
|
21
|
+
table.index(["provider_config_id"], "idx_rewarded_unlock_receipts_provider_config");
|
|
22
|
+
table.index(["unlocked_until"], "idx_rewarded_unlock_receipts_unlocked_until");
|
|
23
|
+
table.index(["watch_session_id"], "idx_rewarded_unlock_receipts_watch_session");
|
|
24
|
+
table.index(["workspace_id","user_id"], "idx_rewarded_unlock_receipts_workspace_user");
|
|
25
|
+
table.foreign(["provider_config_id"], "fk_rewarded_unlock_receipts_provider_config").references(["id"]).inTable("rewarded_provider_configs").onUpdate("RESTRICT").onDelete("SET NULL");
|
|
26
|
+
table.foreign(["user_id"], "fk_rewarded_unlock_receipts_user").references(["id"]).inTable("users").onUpdate("RESTRICT").onDelete("CASCADE");
|
|
27
|
+
table.foreign(["watch_session_id"], "fk_rewarded_unlock_receipts_watch_session").references(["id"]).inTable("rewarded_watch_sessions").onUpdate("RESTRICT").onDelete("SET NULL");
|
|
28
|
+
table.foreign(["workspace_id"], "fk_rewarded_unlock_receipts_workspace").references(["id"]).inTable("workspaces").onUpdate("RESTRICT").onDelete("CASCADE");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
exports.down = async function down(knex) {
|
|
34
|
+
await knex.schema.dropTableIfExists(TABLE_NAME);
|
|
35
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const TABLE_NAME = "rewarded_watch_sessions";
|
|
2
|
+
|
|
3
|
+
exports.up = async function up(knex) {
|
|
4
|
+
const hasCrudTable = await knex.schema.hasTable(TABLE_NAME);
|
|
5
|
+
if (hasCrudTable) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
await knex.schema.createTable(TABLE_NAME, (table) => {
|
|
10
|
+
table.bigIncrements("id").unsigned().primary();
|
|
11
|
+
table.bigInteger("workspace_id").unsigned().notNullable();
|
|
12
|
+
table.bigInteger("user_id").unsigned().notNullable();
|
|
13
|
+
table.string("gate_key", 120).notNullable();
|
|
14
|
+
table.bigInteger("provider_config_id").unsigned().nullable();
|
|
15
|
+
table.string("status", 32).notNullable().defaultTo("started");
|
|
16
|
+
table.timestamp("started_at").notNullable().defaultTo(knex.fn.now());
|
|
17
|
+
table.timestamp("rewarded_at").nullable();
|
|
18
|
+
table.timestamp("completed_at").nullable();
|
|
19
|
+
table.timestamp("closed_at").nullable();
|
|
20
|
+
table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
|
|
21
|
+
table.timestamp("updated_at").notNullable().defaultTo(knex.raw("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"));
|
|
22
|
+
table.index(["user_id"], "fk_rewarded_watch_sessions_user");
|
|
23
|
+
table.index(["gate_key"], "idx_rewarded_watch_sessions_gate");
|
|
24
|
+
table.index(["provider_config_id"], "idx_rewarded_watch_sessions_provider_config");
|
|
25
|
+
table.index(["status"], "idx_rewarded_watch_sessions_status");
|
|
26
|
+
table.index(["workspace_id","user_id"], "idx_rewarded_watch_sessions_workspace_user");
|
|
27
|
+
table.foreign(["provider_config_id"], "fk_rewarded_watch_sessions_provider_config").references(["id"]).inTable("rewarded_provider_configs").onUpdate("RESTRICT").onDelete("SET NULL");
|
|
28
|
+
table.foreign(["user_id"], "fk_rewarded_watch_sessions_user").references(["id"]).inTable("users").onUpdate("RESTRICT").onDelete("CASCADE");
|
|
29
|
+
table.foreign(["workspace_id"], "fk_rewarded_watch_sessions_workspace").references(["id"]).inTable("workspaces").onUpdate("RESTRICT").onDelete("CASCADE");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
exports.down = async function down(knex) {
|
|
35
|
+
await knex.schema.dropTableIfExists(TABLE_NAME);
|
|
36
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/rewarded-core",
|
|
3
|
+
"version": "0.1.120",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
"./shared": "./src/shared/index.js",
|
|
7
|
+
"./server/actions": "./src/server/actions.js",
|
|
8
|
+
"./server/requireRewardedUnlock": "./src/server/support/requireRewardedUnlock.js"
|
|
9
|
+
},
|
|
10
|
+
"description": "Rewarded workflow runtime plus internal CRUD providers for rules, provider configs, watch sessions, and unlock receipts.",
|
|
11
|
+
"jskit": {
|
|
12
|
+
"kind": "runtime",
|
|
13
|
+
"capabilities": {
|
|
14
|
+
"provides": [
|
|
15
|
+
"rewarded.rules",
|
|
16
|
+
"rewarded.provider-configs",
|
|
17
|
+
"rewarded.watch-sessions",
|
|
18
|
+
"rewarded.unlock-receipts",
|
|
19
|
+
"rewarded.core"
|
|
20
|
+
],
|
|
21
|
+
"requires": [
|
|
22
|
+
"runtime.actions",
|
|
23
|
+
"runtime.database",
|
|
24
|
+
"runtime.http",
|
|
25
|
+
"runtime.json-rest-api",
|
|
26
|
+
"rewarded.grant-policy"
|
|
27
|
+
],
|
|
28
|
+
"applicationRequires": [
|
|
29
|
+
"rewarded.grant-policy"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"runtime": {
|
|
33
|
+
"server": {
|
|
34
|
+
"providers": [
|
|
35
|
+
{
|
|
36
|
+
"entrypoint": "src/server/RewardedResources.js",
|
|
37
|
+
"export": "RewardedRulesFeature"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"entrypoint": "src/server/RewardedResources.js",
|
|
41
|
+
"export": "RewardedProviderConfigsFeature"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"entrypoint": "src/server/RewardedResources.js",
|
|
45
|
+
"export": "RewardedWatchSessionsFeature"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"entrypoint": "src/server/RewardedResources.js",
|
|
49
|
+
"export": "RewardedUnlockReceiptsFeature"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"entrypoint": "src/server/RewardedCoreProvider.js",
|
|
53
|
+
"export": "RewardedCoreFeature"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"client": {
|
|
58
|
+
"providers": []
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"metadata": {
|
|
62
|
+
"jskit": {
|
|
63
|
+
"tableOwnership": {
|
|
64
|
+
"tables": [
|
|
65
|
+
{
|
|
66
|
+
"tableName": "rewarded_rules",
|
|
67
|
+
"providerEntrypoint": "src/server/RewardedResources.js",
|
|
68
|
+
"ownershipFilter": "workspace"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"tableName": "rewarded_provider_configs",
|
|
72
|
+
"providerEntrypoint": "src/server/RewardedResources.js",
|
|
73
|
+
"ownershipFilter": "workspace"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"tableName": "rewarded_watch_sessions",
|
|
77
|
+
"providerEntrypoint": "src/server/RewardedResources.js",
|
|
78
|
+
"ownershipFilter": "workspace_user"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"tableName": "rewarded_unlock_receipts",
|
|
82
|
+
"providerEntrypoint": "src/server/RewardedResources.js",
|
|
83
|
+
"ownershipFilter": "workspace_user"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"apiSummary": {
|
|
89
|
+
"surfaces": [
|
|
90
|
+
{
|
|
91
|
+
"subpath": "./shared",
|
|
92
|
+
"summary": "Exports Rewarded shared CRUD resources."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"subpath": "./server/actions",
|
|
96
|
+
"summary": "Exports Rewarded workflow action identifiers and validators."
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"subpath": "./server/requireRewardedUnlock",
|
|
100
|
+
"summary": "Exports the server helper that enforces a rewarded unlock before a protected feature action continues."
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"capabilities": [
|
|
104
|
+
"rewarded.rules",
|
|
105
|
+
"rewarded.provider-configs",
|
|
106
|
+
"rewarded.watch-sessions",
|
|
107
|
+
"rewarded.unlock-receipts",
|
|
108
|
+
"rewarded.core"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"migrations": {
|
|
113
|
+
"directories": [
|
|
114
|
+
"migrations"
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"dependencies": {
|
|
119
|
+
"@jskit-ai/crud-core": "0.1.195",
|
|
120
|
+
"@jskit-ai/database-runtime": "0.1.184",
|
|
121
|
+
"@jskit-ai/json-rest-api-core": "0.1.128",
|
|
122
|
+
"@jskit-ai/resource-crud-core": "0.1.126",
|
|
123
|
+
"@jskit-ai/workspaces-core": "0.1.162"
|
|
124
|
+
},
|
|
125
|
+
"peerDependencies": {
|
|
126
|
+
"@jskit-ai/auth-core": "0.1.182",
|
|
127
|
+
"@jskit-ai/http-runtime": "0.1.182",
|
|
128
|
+
"@jskit-ai/kernel": "0.1.184"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineFeature } from "@jskit-ai/kernel/server/features";
|
|
2
|
+
|
|
3
|
+
import { createService } from "./service.js";
|
|
4
|
+
import { createRewardedActions } from "./actions.js";
|
|
5
|
+
import { registerRoutes } from "./registerRoutes.js";
|
|
6
|
+
|
|
7
|
+
const RewardedCoreFeature = defineFeature({
|
|
8
|
+
id: "rewarded.core",
|
|
9
|
+
domain: "rewarded",
|
|
10
|
+
requires: {
|
|
11
|
+
http: "runtime.http",
|
|
12
|
+
grantPolicy: "rewarded.grant-policy",
|
|
13
|
+
providerConfigs: "rewarded.provider-configs",
|
|
14
|
+
rules: "rewarded.rules",
|
|
15
|
+
unlockReceipts: "rewarded.unlock-receipts",
|
|
16
|
+
watchSessions: "rewarded.watch-sessions"
|
|
17
|
+
},
|
|
18
|
+
provides: {
|
|
19
|
+
rewarded: "rewarded.core"
|
|
20
|
+
},
|
|
21
|
+
setup({ http, grantPolicy, providerConfigs, rules, unlockReceipts, watchSessions }) {
|
|
22
|
+
if (typeof grantPolicy.authorizeGrant !== "function") {
|
|
23
|
+
throw new TypeError("rewarded.grant-policy requires authorizeGrant.");
|
|
24
|
+
}
|
|
25
|
+
const rewarded = createService({
|
|
26
|
+
authorizeGrant: (input) => grantPolicy.authorizeGrant(input),
|
|
27
|
+
rewardedRulesRepository: rules.repository,
|
|
28
|
+
rewardedProviderConfigsRepository: providerConfigs.repository,
|
|
29
|
+
rewardedWatchSessionsRepository: watchSessions.repository,
|
|
30
|
+
rewardedUnlockReceiptsRepository: unlockReceipts.repository
|
|
31
|
+
});
|
|
32
|
+
registerRoutes(http.router, {
|
|
33
|
+
routeOwnershipFilter: "workspace_user",
|
|
34
|
+
routeSurface: "app",
|
|
35
|
+
routeRelativePath: "rewarded"
|
|
36
|
+
});
|
|
37
|
+
return { rewarded };
|
|
38
|
+
},
|
|
39
|
+
actions({ rewarded }) {
|
|
40
|
+
return createRewardedActions({ rewarded });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export { RewardedCoreFeature };
|